utils.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import ModeratorToolsModal from './modals/moderator-tools.js';
  2. import log from "@converse/headless/log";
  3. import tpl_spinner from 'templates/spinner.js';
  4. import { Model } from '@converse/skeletor/src/model.js';
  5. import { __ } from 'i18n';
  6. import { _converse, api, converse } from "@converse/headless/core";
  7. import { html } from "lit";
  8. import { parseMessageForCommands } from 'plugins/chatview/utils.js';
  9. import { setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js';
  10. const { Strophe, $pres, $iq, sizzle, u } = converse.env;
  11. const COMMAND_TO_AFFILIATION = {
  12. 'admin': 'admin',
  13. 'ban': 'outcast',
  14. 'member': 'member',
  15. 'owner': 'owner',
  16. 'revoke': 'none'
  17. };
  18. const COMMAND_TO_ROLE = {
  19. 'deop': 'participant',
  20. 'kick': 'none',
  21. 'mute': 'visitor',
  22. 'op': 'moderator',
  23. 'voice': 'participant'
  24. };
  25. function setMUCDomain (domain, controlboxview) {
  26. controlboxview.querySelector('converse-rooms-list')
  27. .model.save('muc_domain', Strophe.getDomainFromJid(domain));
  28. }
  29. function setMUCDomainFromDisco (controlboxview) {
  30. /* Check whether service discovery for the user's domain
  31. * returned MUC information and use that to automatically
  32. * set the MUC domain in the "Add groupchat" modal.
  33. */
  34. function featureAdded (feature) {
  35. if (!feature) {
  36. return;
  37. }
  38. if (feature.get('var') === Strophe.NS.MUC) {
  39. feature.entity.getIdentity('conference', 'text').then(identity => {
  40. if (identity) {
  41. setMUCDomain(feature.get('from'), controlboxview);
  42. }
  43. });
  44. }
  45. }
  46. api.waitUntil('discoInitialized')
  47. .then(() => {
  48. api.listen.on('serviceDiscovered', featureAdded);
  49. // Features could have been added before the controlbox was
  50. // initialized. We're only interested in MUC
  51. _converse.disco_entities.each(entity => featureAdded(entity.features.findWhere({ 'var': Strophe.NS.MUC })));
  52. })
  53. .catch(e => log.error(e));
  54. }
  55. export function fetchAndSetMUCDomain (controlboxview) {
  56. if (controlboxview.model.get('connected')) {
  57. if (!controlboxview.querySelector('converse-rooms-list').model.get('muc_domain')) {
  58. if (api.settings.get('muc_domain') === undefined) {
  59. setMUCDomainFromDisco(controlboxview);
  60. } else {
  61. setMUCDomain(api.settings.get('muc_domain'), controlboxview);
  62. }
  63. }
  64. }
  65. }
  66. export function getNicknameRequiredTemplate (model) {
  67. const jid = model.get('jid');
  68. if (api.settings.get('muc_show_logs_before_join')) {
  69. return html`<converse-muc-chatarea jid="${jid}"></converse-muc-chatarea>`;
  70. } else {
  71. return html`<converse-muc-nickname-form jid="${jid}"></converse-muc-nickname-form>`;
  72. }
  73. }
  74. export function getChatRoomBodyTemplate (o) {
  75. const view = o.model.session.get('view');
  76. const jid = o.model.get('jid');
  77. const RS = converse.ROOMSTATUS;
  78. const conn_status = o.model.session.get('connection_status');
  79. if (view === converse.MUC.VIEWS.CONFIG) {
  80. return html`<converse-muc-config-form class="muc-form-container" jid="${jid}"></converse-muc-config-form>`;
  81. } else if (view === converse.MUC.VIEWS.BOOKMARK) {
  82. return html`<converse-muc-bookmark-form class="muc-form-container" jid="${jid}"></converse-muc-bookmark-form>`;
  83. } else {
  84. return html`
  85. ${ conn_status == RS.PASSWORD_REQUIRED ? html`<converse-muc-password-form class="muc-form-container" jid="${jid}"></converse-muc-password-form>` : '' }
  86. ${ conn_status == RS.ENTERED ? html`<converse-muc-chatarea jid="${jid}"></converse-muc-chatarea>` : '' }
  87. ${ conn_status == RS.CONNECTING ? tpl_spinner() : '' }
  88. ${ conn_status == RS.NICKNAME_REQUIRED ? getNicknameRequiredTemplate(o.model) : '' }
  89. ${ conn_status == RS.DISCONNECTED ? html`<converse-muc-disconnected jid="${jid}"></converse-muc-disconnected>` : '' }
  90. ${ conn_status == RS.BANNED ? html`<converse-muc-disconnected jid="${jid}"></converse-muc-disconnected>` : '' }
  91. ${ conn_status == RS.DESTROYED ? html`<converse-muc-destroyed jid="${jid}"></converse-muc-destroyed>` : '' }
  92. `;
  93. }
  94. }
  95. export function getAutoCompleteListItem (text, input) {
  96. input = input.trim();
  97. const element = document.createElement('li');
  98. element.setAttribute('aria-selected', 'false');
  99. if (api.settings.get('muc_mention_autocomplete_show_avatar')) {
  100. const img = document.createElement('img');
  101. let dataUri = 'data:' + _converse.DEFAULT_IMAGE_TYPE + ';base64,' + _converse.DEFAULT_IMAGE;
  102. if (_converse.vcards) {
  103. const vcard = _converse.vcards.findWhere({ 'nickname': text });
  104. if (vcard) dataUri = 'data:' + vcard.get('image_type') + ';base64,' + vcard.get('image');
  105. }
  106. img.setAttribute('src', dataUri);
  107. img.setAttribute('width', '22');
  108. img.setAttribute('class', 'avatar avatar-autocomplete');
  109. element.appendChild(img);
  110. }
  111. const regex = new RegExp('(' + input + ')', 'ig');
  112. const parts = input ? text.split(regex) : [text];
  113. parts.forEach(txt => {
  114. if (input && txt.match(regex)) {
  115. const match = document.createElement('mark');
  116. match.textContent = txt;
  117. element.appendChild(match);
  118. } else {
  119. element.appendChild(document.createTextNode(txt));
  120. }
  121. });
  122. return element;
  123. }
  124. export async function getAutoCompleteList () {
  125. const models = [...(await api.rooms.get()), ...(await api.contacts.get())];
  126. const jids = [...new Set(models.map(o => Strophe.getDomainFromJid(o.get('jid'))))];
  127. return jids;
  128. }
  129. export async function fetchCommandForm (command) {
  130. const node = command.node;
  131. const jid = command.jid;
  132. const stanza = $iq({
  133. 'type': 'set',
  134. 'to': jid
  135. }).c('command', {
  136. 'xmlns': Strophe.NS.ADHOC,
  137. 'node': node,
  138. 'action': 'execute'
  139. });
  140. try {
  141. const iq = await api.sendIQ(stanza);
  142. const cmd_el = sizzle(`command[xmlns="${Strophe.NS.ADHOC}"]`, iq).pop();
  143. command.sessionid = cmd_el.getAttribute('sessionid');
  144. command.instructions = sizzle('x[type="form"][xmlns="jabber:x:data"] instructions', cmd_el).pop()?.textContent;
  145. command.fields = sizzle('x[type="form"][xmlns="jabber:x:data"] field', cmd_el)
  146. .map(f => u.xForm2TemplateResult(f, cmd_el));
  147. } catch (e) {
  148. if (e === null) {
  149. log.error(`Error: timeout while trying to execute command for ${jid}`);
  150. } else {
  151. log.error(`Error while trying to execute command for ${jid}`);
  152. log.error(e);
  153. }
  154. command.fields = [];
  155. }
  156. }
  157. function setRole (muc, command, args, required_affiliations = [], required_roles = []) {
  158. const role = COMMAND_TO_ROLE[command];
  159. if (!role) {
  160. throw Error(`ChatRoomView#setRole called with invalid command: ${command}`);
  161. }
  162. if (!muc.verifyAffiliations(required_affiliations) || !muc.verifyRoles(required_roles)) {
  163. return false;
  164. }
  165. if (!muc.validateRoleOrAffiliationChangeArgs(command, args)) {
  166. return false;
  167. }
  168. const nick_or_jid = muc.getNickOrJIDFromCommandArgs(args);
  169. if (!nick_or_jid) {
  170. return false;
  171. }
  172. const reason = args.split(nick_or_jid, 2)[1].trim();
  173. // We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs
  174. const occupant = muc.getOccupant(nick_or_jid);
  175. muc.setRole(occupant, role, reason, undefined, e => muc.onCommandError(e));
  176. return true;
  177. }
  178. function verifyAndSetAffiliation (muc, command, args, required_affiliations) {
  179. const affiliation = COMMAND_TO_AFFILIATION[command];
  180. if (!affiliation) {
  181. throw Error(`verifyAffiliations called with invalid command: ${command}`);
  182. }
  183. if (!muc.verifyAffiliations(required_affiliations)) {
  184. return false;
  185. }
  186. if (!muc.validateRoleOrAffiliationChangeArgs(command, args)) {
  187. return false;
  188. }
  189. const nick_or_jid = muc.getNickOrJIDFromCommandArgs(args);
  190. if (!nick_or_jid) {
  191. return false;
  192. }
  193. let jid;
  194. const reason = args.split(nick_or_jid, 2)[1].trim();
  195. const occupant = muc.getOccupant(nick_or_jid);
  196. if (occupant) {
  197. jid = occupant.get('jid');
  198. } else {
  199. if (u.isValidJID(nick_or_jid)) {
  200. jid = nick_or_jid;
  201. } else {
  202. const message = __(
  203. "Couldn't find a participant with that nickname. " + 'They might have left the groupchat.'
  204. );
  205. muc.createMessage({ message, 'type': 'error' });
  206. return;
  207. }
  208. }
  209. const attrs = { jid, reason };
  210. if (occupant && api.settings.get('auto_register_muc_nickname')) {
  211. attrs['nick'] = occupant.get('nick');
  212. }
  213. setAffiliation(affiliation, muc.get('jid'), [attrs])
  214. .then(() => muc.occupants.fetchMembers())
  215. .catch(err => muc.onCommandError(err));
  216. }
  217. export function showModeratorToolsModal (muc, affiliation) {
  218. if (!muc.verifyRoles(['moderator'])) {
  219. return;
  220. }
  221. let modal = api.modal.get(ModeratorToolsModal.id);
  222. if (modal) {
  223. modal.model.set({ affiliation });
  224. } else {
  225. const model = new Model({ affiliation });
  226. modal = api.modal.create(ModeratorToolsModal, { model, muc });
  227. }
  228. modal.show();
  229. }
  230. export function parseMessageForMUCCommands (muc, text) {
  231. if (
  232. api.settings.get('muc_disable_slash_commands') &&
  233. !Array.isArray(api.settings.get('muc_disable_slash_commands'))
  234. ) {
  235. return parseMessageForCommands(muc, text);
  236. }
  237. text = text.replace(/^\s*/, '');
  238. const command = (text.match(/^\/([a-zA-Z]*) ?/) || ['']).pop().toLowerCase();
  239. if (!command) {
  240. return false;
  241. }
  242. const args = text.slice(('/' + command).length + 1).trim();
  243. if (!muc.getAllowedCommands().includes(command)) {
  244. return false;
  245. }
  246. switch (command) {
  247. case 'admin': {
  248. verifyAndSetAffiliation(muc, command, args, ['owner']);
  249. break;
  250. }
  251. case 'ban': {
  252. verifyAndSetAffiliation(muc, command, args, ['admin', 'owner']);
  253. break;
  254. }
  255. case 'modtools': {
  256. showModeratorToolsModal(muc, args);
  257. break;
  258. }
  259. case 'deop': {
  260. // FIXME: /deop only applies to setting a moderators
  261. // role to "participant" (which only admin/owner can
  262. // do). Moderators can however set non-moderator's role
  263. // to participant (e.g. visitor => participant).
  264. // Currently we don't distinguish between these two
  265. // cases.
  266. setRole(muc, command, args, ['admin', 'owner']);
  267. break;
  268. }
  269. case 'destroy': {
  270. if (!muc.verifyAffiliations(['owner'])) {
  271. break;
  272. }
  273. const chatview = _converse.chatboxviews.get(muc.get('jid'));
  274. chatview.destroy().catch(e => muc.onCommandError(e));
  275. break;
  276. }
  277. case 'help': {
  278. muc.set({ 'show_help_messages': false }, { 'silent': true });
  279. muc.set({ 'show_help_messages': true });
  280. break;
  281. }
  282. case 'kick': {
  283. setRole(muc, command, args, [], ['moderator']);
  284. break;
  285. }
  286. case 'mute': {
  287. setRole(muc, command, args, [], ['moderator']);
  288. break;
  289. }
  290. case 'member': {
  291. verifyAndSetAffiliation(muc, command, args, ['admin', 'owner']);
  292. break;
  293. }
  294. case 'nick': {
  295. if (!muc.verifyRoles(['visitor', 'participant', 'moderator'])) {
  296. break;
  297. } else if (args.length === 0) {
  298. // e.g. Your nickname is "coolguy69"
  299. const message = __('Your nickname is "%1$s"', muc.get('nick'));
  300. muc.createMessage({ message, 'type': 'error' });
  301. } else {
  302. const jid = Strophe.getBareJidFromJid(muc.get('jid'));
  303. api.send(
  304. $pres({
  305. from: _converse.connection.jid,
  306. to: `${jid}/${args}`,
  307. id: u.getUniqueId()
  308. }).tree()
  309. );
  310. }
  311. break;
  312. }
  313. case 'owner':
  314. verifyAndSetAffiliation(muc, command, args, ['owner']);
  315. break;
  316. case 'op': {
  317. setRole(muc, command, args, ['admin', 'owner']);
  318. break;
  319. }
  320. case 'register': {
  321. if (args.length > 1) {
  322. muc.createMessage({
  323. 'message': __('Error: invalid number of arguments'),
  324. 'type': 'error'
  325. });
  326. } else {
  327. muc.registerNickname().then(err_msg => {
  328. err_msg && muc.createMessage({ 'message': err_msg, 'type': 'error' });
  329. });
  330. }
  331. break;
  332. }
  333. case 'revoke': {
  334. verifyAndSetAffiliation(muc, command, args, ['admin', 'owner']);
  335. break;
  336. }
  337. case 'topic':
  338. case 'subject':
  339. muc.setSubject(args);
  340. break;
  341. case 'voice': {
  342. setRole(muc, command, args, [], ['moderator']);
  343. break;
  344. }
  345. default:
  346. return parseMessageForCommands(muc, text);
  347. }
  348. return true;
  349. }