verify.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import { randomBytes } from 'crypto';
  2. import { MessageEmbed, MessageActionRow, MessageButton, Permissions } from 'discord.js';
  3. import db from '../util/database.js';
  4. import verify from '../functions/verify.js';
  5. import { got, oauthVerify, allowDelete, escapeFormatting } from '../util/functions.js';
  6. /**
  7. * Processes the "verify" command.
  8. * @param {import('../util/i18n.js').default} lang - The user language.
  9. * @param {import('discord.js').Message} msg - The Discord message.
  10. * @param {String[]} args - The command arguments.
  11. * @param {String} line - The command as plain text.
  12. * @param {import('../util/wiki.js').default} wiki - The wiki for the message.
  13. */
  14. function cmd_verify(lang, msg, args, line, wiki) {
  15. if ( !msg.inGuild() || msg.defaultSettings ) return this.LINK(lang, msg, line, wiki);
  16. if ( !msg.guild.me.permissions.has(Permissions.FLAGS.MANAGE_ROLES) ) {
  17. if ( msg.isAdmin() ) {
  18. console.log( msg.guildId + ': Missing permissions - MANAGE_ROLES' );
  19. msg.replyMsg( lang.get('general.missingperm') + ' `MANAGE_ROLES`' );
  20. }
  21. else if ( !msg.onlyVerifyCommand ) this.LINK(lang, msg, line, wiki);
  22. return;
  23. }
  24. db.query( 'SELECT logchannel, flags, onsuccess, onmatch, role, editcount, postcount, usergroup, accountage, rename FROM verification LEFT JOIN verifynotice ON verification.guild = verifynotice.guild WHERE verification.guild = $1 AND channel LIKE $2 ORDER BY configid ASC', [msg.guildId, '%|' + ( msg.channel.isThread() ? msg.channel.parentId : msg.channelId ) + '|%'] ).then( ({rows}) => {
  25. if ( !rows.length ) {
  26. if ( msg.onlyVerifyCommand ) return;
  27. return msg.replyMsg( lang.get('verify.missing') + ( msg.isAdmin() ? '\n`' + ( patreonGuildsPrefix.get(msg.guildId) ?? process.env.prefix ) + 'verification`' : '' ) );
  28. }
  29. if ( wiki.hasOAuth2() && process.env.dashboard ) {
  30. let oauth = [wiki.hostname + wiki.pathname.slice(0, -1)];
  31. if ( wiki.isWikimedia() ) oauth.push('wikimedia');
  32. if ( wiki.isMiraheze() ) oauth.push('miraheze');
  33. if ( process.env['oauth_' + ( oauth[1] || oauth[0] )] && process.env['oauth_' + ( oauth[1] || oauth[0] ) + '_secret'] ) {
  34. return db.query( 'SELECT token FROM oauthusers WHERE userid = $1 AND site = $2', [msg.author.id, ( oauth[1] || oauth[0] )] ).then( ({rows: [row]}) => {
  35. if ( row?.token ) return got.post( wiki + 'rest.php/oauth2/access_token', {
  36. form: {
  37. grant_type: 'refresh_token', refresh_token: row.token,
  38. redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
  39. client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )],
  40. client_secret: process.env['oauth_' + ( oauth[1] || oauth[0] ) + '_secret']
  41. }
  42. } ).then( response => {
  43. var body = response.body;
  44. if ( response.statusCode !== 200 || !body?.access_token ) {
  45. console.log( '- ' + response.statusCode + ': Error while refreshing the mediawiki token: ' + ( body?.message || body?.error ) );
  46. return Promise.reject(row);
  47. }
  48. if ( body?.refresh_token ) db.query( 'UPDATE oauthusers SET token = $1 WHERE userid = $2 AND site = $3', [body.refresh_token, msg.author.id, ( oauth[1] || oauth[0] )] ).then( () => {
  49. console.log( '- Dashboard: OAuth2 token for ' + msg.author.id + ' successfully updated.' );
  50. }, dberror => {
  51. console.log( '- Dashboard: Error while updating the OAuth2 token for ' + msg.author.id + ': ' + dberror );
  52. } );
  53. return verifyOauthUser('', body.access_token, {
  54. wiki: wiki.href, channel: msg.channel,
  55. user: msg.author.id, sourceMessage: msg,
  56. fail: () => msg.replyMsg( lang.get('verify.error_reply'), false, false ).then( message => {
  57. if ( message ) message.reactEmoji('error');
  58. } )
  59. });
  60. }, error => {
  61. console.log( '- Error while refreshing the mediawiki token: ' + error );
  62. return Promise.reject(row);
  63. } );
  64. return Promise.reject(row);
  65. }, dberror => {
  66. console.log( '- Error while getting the OAuth2 token: ' + dberror );
  67. return Promise.reject();
  68. } ).catch( row => {
  69. if ( row ) {
  70. if ( !row?.hasOwnProperty?.('token') ) console.log( '- Error while checking the OAuth2 refresh token: ' + row );
  71. else if ( row.token ) db.query( 'DELETE FROM oauthusers WHERE userid = $1 AND site = $2', [msg.author.id, ( oauth[1] || oauth[0] )] ).then( () => {
  72. console.log( '- Dashboard: OAuth2 token for ' + msg.author.id + ' successfully deleted.' );
  73. }, dberror => {
  74. console.log( '- Dashboard: Error while deleting the OAuth2 token for ' + msg.author.id + ': ' + dberror );
  75. } );
  76. }
  77. let state = `${oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
  78. while ( oauthVerify.has(state) ) {
  79. state = `${oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
  80. }
  81. oauthVerify.set(state, {
  82. state, wiki: wiki.href,
  83. channel: msg.channel,
  84. user: msg.author.id
  85. });
  86. msg.client.shard.send({id: 'verifyUser', state, user: ( row?.token === null ? '' : msg.author.id )});
  87. let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
  88. response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
  89. client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )], state
  90. }).toString();
  91. return msg.member.send( {
  92. content: lang.get('verify.oauth_message_dm', escapeFormatting(msg.guild.name)) + '\n<' + oauthURL + '>',
  93. components: [new MessageActionRow().addComponents(
  94. new MessageButton().setLabel(lang.get('verify.oauth_button')).setEmoji('🔗').setStyle('LINK').setURL(oauthURL)
  95. )]
  96. } ).then( message => {
  97. msg.reactEmoji('📩');
  98. allowDelete(message, msg.author.id);
  99. setTimeout( () => msg.delete().catch(log_error), 60_000 ).unref();
  100. }, error => {
  101. if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
  102. return msg.replyMsg( lang.get('verify.oauth_private') );
  103. }
  104. log_error(error);
  105. msg.reactEmoji('error');
  106. } );
  107. } );
  108. }
  109. }
  110. var username = args.join(' ').replace( /_/g, ' ' ).trim().replace( /^<\s*(.*)\s*>$/, '$1' ).replace( /^@/, '' ).split('#')[0].substring(0, 250).trim();
  111. if ( /^(?:https?:)?\/\/([a-z\d-]{1,50})\.(?:gamepedia\.com\/|(?:fandom\.com|wikia\.org)\/(?:[a-z-]{1,8}\/)?(?:wiki\/)?)/.test(username) ) {
  112. username = decodeURIComponent( username.replace( /^(?:https?:)?\/\/([a-z\d-]{1,50})\.(?:gamepedia\.com\/|(?:fandom\.com|wikia\.org)\/(?:[a-z-]{1,8}\/)?(?:wiki\/)?)/, '' ) );
  113. }
  114. if ( wiki.isGamepedia() ) username = username.replace( /^userprofile\s*:\s*/i, '' );
  115. if ( !username.trim() ) {
  116. args[0] = line.split(' ')[0];
  117. if ( args[0] === 'verification' ) args[0] = ( lang.localNames.verify || 'verify' );
  118. return this.help(lang, msg, args, line, wiki);
  119. }
  120. msg.reactEmoji('⏳').then( reaction => {
  121. verify(lang, msg.channel, msg.member, username, wiki, rows).then( result => {
  122. if ( result.oauth.length ) {
  123. return db.query( 'SELECT token FROM oauthusers WHERE userid = $1 AND site = $2', [msg.author.id, ( result.oauth[1] || result.oauth[0] )] ).then( ({rows: [row]}) => {
  124. if ( row?.token ) return got.post( wiki + 'rest.php/oauth2/access_token', {
  125. form: {
  126. grant_type: 'refresh_token', refresh_token: row.token,
  127. redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
  128. client_id: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] )],
  129. client_secret: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] ) + '_secret']
  130. }
  131. } ).then( response => {
  132. var body = response.body;
  133. if ( response.statusCode !== 200 || !body?.access_token ) {
  134. console.log( '- ' + response.statusCode + ': Error while refreshing the mediawiki token: ' + ( body?.message || body?.error ) );
  135. return Promise.reject(row);
  136. }
  137. if ( body?.refresh_token ) db.query( 'UPDATE oauthusers SET token = $1 WHERE userid = $2 AND site = $3', [body.refresh_token, msg.author.id, ( result.oauth[1] || result.oauth[0] )] ).then( () => {
  138. console.log( '- Dashboard: OAuth2 token for ' + msg.author.id + ' successfully updated.' );
  139. }, dberror => {
  140. console.log( '- Dashboard: Error while updating the OAuth2 token for ' + msg.author.id + ': ' + dberror );
  141. } );
  142. return verifyOauthUser('', body.access_token, {
  143. wiki: wiki.href, channel: msg.channel,
  144. user: msg.author.id, sourceMessage: msg,
  145. fail: () => msg.replyMsg( lang.get('verify.error_reply'), false, false ).then( message => {
  146. if ( message ) message.reactEmoji('error');
  147. } )
  148. });
  149. }, error => {
  150. console.log( '- Error while refreshing the mediawiki token: ' + error );
  151. return Promise.reject(row);
  152. } );
  153. return Promise.reject(row);
  154. }, dberror => {
  155. console.log( '- Error while getting the OAuth2 token: ' + dberror );
  156. return Promise.reject();
  157. } ).catch( row => {
  158. if ( row ) {
  159. if ( !row?.hasOwnProperty?.('token') ) console.log( '- Error while checking the OAuth2 refresh token: ' + row );
  160. else if ( row.token ) db.query( 'DELETE FROM oauthusers WHERE userid = $1 AND site = $2', [msg.author.id, ( result.oauth[1] || result.oauth[0] )] ).then( () => {
  161. console.log( '- Dashboard: OAuth2 token for ' + msg.author.id + ' successfully deleted.' );
  162. }, dberror => {
  163. console.log( '- Dashboard: Error while deleting the OAuth2 token for ' + msg.author.id + ': ' + dberror );
  164. } );
  165. }
  166. let state = `${result.oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
  167. while ( oauthVerify.has(state) ) {
  168. state = `${result.oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
  169. }
  170. oauthVerify.set(state, {
  171. state, wiki: wiki.href,
  172. channel: msg.channel,
  173. user: msg.author.id
  174. });
  175. msg.client.shard.send({id: 'verifyUser', state, user: ( row?.token === null ? '' : msg.author.id )});
  176. let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
  177. response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
  178. client_id: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] )], state
  179. }).toString();
  180. msg.member.send( {
  181. content: lang.get('verify.oauth_message_dm', escapeFormatting(msg.guild.name)) + '\n<' + oauthURL + '>',
  182. components: [new MessageActionRow().addComponents(
  183. new MessageButton().setLabel(lang.get('verify.oauth_button')).setEmoji('🔗').setStyle('LINK').setURL(oauthURL)
  184. )]
  185. } ).then( message => {
  186. msg.reactEmoji('📩');
  187. allowDelete(message, msg.author.id);
  188. setTimeout( () => msg.delete().catch(log_error), 60_000 ).unref();
  189. }, error => {
  190. if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
  191. return msg.replyMsg( lang.get('verify.oauth_private') );
  192. }
  193. log_error(error);
  194. msg.reactEmoji('error');
  195. } );
  196. } );
  197. }
  198. else if ( result.reaction ) msg.reactEmoji(result.reaction);
  199. else {
  200. var options = {
  201. content: msg.member.toString() + ', ' + result.content,
  202. embeds: [result.embed],
  203. components: [],
  204. allowedMentions: {
  205. users: [msg.author.id],
  206. repliedUser: true
  207. }
  208. };
  209. if ( result.add_button ) options.components.push(new MessageActionRow().addComponents(
  210. new MessageButton().setLabel(lang.get('verify.button_again')).setEmoji('🔂').setStyle('PRIMARY').setCustomId('verify_again')
  211. ));
  212. if ( result.send_private ) {
  213. let dmEmbeds = [new MessageEmbed(result.embed)];
  214. if ( options.embeds[0] ) {
  215. dmEmbeds.push(new MessageEmbed(options.embeds[0]));
  216. dmEmbeds[0].fields.forEach( field => {
  217. field.value = field.value.replace( /<@&(\d+)>/g, (mention, id) => {
  218. if ( !msg.guild.roles.cache.has(id) ) return mention;
  219. return escapeFormatting('@' + msg.guild.roles.cache.get(id)?.name);
  220. } );
  221. } );
  222. }
  223. msg.member.send( {content: msg.channel.toString() + '; ' + result.content, embeds: dmEmbeds, components: []} ).then( message => {
  224. msg.reactEmoji('📩');
  225. allowDelete(message, msg.author.id);
  226. setTimeout( () => msg.delete().catch(log_error), 60_000 ).unref();
  227. }, error => {
  228. if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
  229. return msg.replyMsg( options, false, false );
  230. }
  231. log_error(error);
  232. msg.reactEmoji('error');
  233. } ).then( message => {
  234. if ( !result.logging.channel || !msg.guild.channels.cache.has(result.logging.channel) ) return;
  235. if ( message ) {
  236. if ( result.logging.embed ) result.logging.embed.addField(message.url, '<#' + msg.channelId + '>');
  237. else result.logging.content += '\n<#' + msg.channelId + '> – <' + message.url + '>';
  238. }
  239. msg.guild.channels.cache.get(result.logging.channel).send( {
  240. content: result.logging.content,
  241. embeds: ( result.logging.embed ? [result.logging.embed] : [] )
  242. } ).catch(log_error);
  243. } );
  244. }
  245. else msg.replyMsg( options, false, false ).then( message => {
  246. if ( !result.logging.channel || !msg.guild.channels.cache.has(result.logging.channel) ) return;
  247. if ( message ) {
  248. if ( result.logging.embed ) result.logging.embed.addField(message.url, '<#' + msg.channelId + '>');
  249. else result.logging.content += '\n<#' + msg.channelId + '> – <' + message.url + '>';
  250. }
  251. msg.guild.channels.cache.get(result.logging.channel).send( {
  252. content: result.logging.content,
  253. embeds: ( result.logging.embed ? [result.logging.embed] : [] )
  254. } ).catch(log_error);
  255. } );
  256. }
  257. if ( reaction ) reaction.removeEmoji();
  258. }, error => {
  259. console.log( '- Error during the verifications: ' + error );
  260. msg.replyMsg( lang.get('verify.error_reply'), false, false ).then( message => {
  261. if ( message ) message.reactEmoji('error');
  262. } );
  263. } );
  264. } );
  265. }, dberror => {
  266. console.log( '- Error while getting the verifications: ' + dberror );
  267. msg.replyMsg( lang.get('verify.error_reply'), false, false ).then( message => {
  268. if ( message ) message.reactEmoji('error');
  269. } );
  270. } );
  271. }
  272. export default {
  273. name: 'verify',
  274. everyone: true,
  275. pause: false,
  276. owner: false,
  277. run: cmd_verify
  278. };