From 040d3fa184ee903a5738176ec380632c3c1be758 Mon Sep 17 00:00:00 2001 From: crowetic Date: Mon, 6 Jan 2025 18:43:54 -0800 Subject: [PATCH] added kick and ban functionality - NEEDS TESTING PRIOR TO RELEASE. --- assets/js/AdminBoard.js | 172 +++++++++++++++++++++++++++++---------- assets/js/MinterBoard.js | 27 ++++-- assets/js/QortalApi.js | 101 ++++++++++++++++++++++- 3 files changed, 251 insertions(+), 49 deletions(-) diff --git a/assets/js/AdminBoard.js b/assets/js/AdminBoard.js index 8e50bf4..09185e8 100644 --- a/assets/js/AdminBoard.js +++ b/assets/js/AdminBoard.js @@ -575,6 +575,7 @@ const validateMinterName = async(minterName) => { return name } catch (error){ console.error(`extracting name from name info: ${minterName} failed.`, error) + return null } } @@ -771,39 +772,6 @@ const fetchEncryptedComments = async (cardIdentifier) => { } } -// display the comments on the card, with passed cardIdentifier to identify the card -------------- -// const displayEncryptedComments = async (cardIdentifier) => { -// try { -// const comments = await fetchEncryptedComments(cardIdentifier) -// const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`) - -// for (const comment of comments) { -// const commentDataResponse = await qortalRequest({ -// action: "FETCH_QDN_RESOURCE", -// name: comment.name, -// service: "MAIL_PRIVATE", -// identifier: comment.identifier, -// encoding: "base64" -// }) - -// const decryptedCommentData = await decryptAndParseObject(commentDataResponse) -// const timestampCheck = comment.updated || comment.created || 0 -// const timestamp = await timestampToHumanReadableDate(timestampCheck) -// //TODO - add fetching of poll results and checking to see if the commenter has voted and display it as 'supports minter' section. -// const commentHTML = ` -//
-//

${decryptedCommentData.creator}:

-//

${decryptedCommentData.content}

-//

${timestamp}

-//
-// ` -// commentsContainer.insertAdjacentHTML('beforeend', commentHTML) -// } -// } catch (error) { -// console.error(`Error displaying comments (or no comments) for ${cardIdentifier}:`, error) -// } -// } -//TODO testing this update to the comments fetching to improve performance by leveraging promise.all const displayEncryptedComments = async (cardIdentifier) => { try { const comments = await fetchEncryptedComments(cardIdentifier) @@ -943,23 +911,129 @@ const processQortalLinkForRendering = async (link) => { return link } -const getMinterAvatar = async (minterName) => { - const avatarUrl = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar` - try { - const response = await fetch(avatarUrl, { method: 'HEAD' }) +const checkAndDisplayRemoveActions = async (adminYes, creator, cardIdentifier) => { + const latestBlockInfo = await getLatestBlockInfo() + const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT + let minAdminCount = 9 + const minterAdmins = await fetchMinterGroupAdmins() - if (response.ok) { - return `User Avatar` + if ((minterAdmins) && (minterAdmins.length === 1)){ + console.warn(`simply a double-check that there is only one MINTER group admin, in which case the group hasn't been transferred to null...keeping default minAdminCount of: ${minAdminCount}`) + + } else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){ + const totalAdmins = minterAdmins.length + const fortyPercent = totalAdmins * 0.40 + minAdminCount = Math.round(fortyPercent) + console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`) + } + //TODO verify the above functionality to calculate 40% of MINTER group admins, and use that for minAdminCount + + if (adminYes >= minAdminCount && userState.isMinterAdmin && !isBlockPassed) { + const removeButtonHtml = createRemoveButtonHtml(creator, cardIdentifier) + return removeButtonHtml + } + return '' +} + +const createRemoveButtonHtml = (name, cardIdentifier) => { + return ` +
+ + +
+ ` +} + +const handleKickMinter = async (minterName) => { + try { + // Optional block check + const { height: currentHeight } = await getLatestBlockInfo() + if (currentHeight <= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT) { + console.log(`block height is under the removal featureTrigger height`) + } + + // Get the minter address from name info + const minterInfo = await getNameInfo(minterName) + const minterAddress = minterInfo?.owner + if (!minterAddress) { + alert(`No valid address found for minter name: ${minterName}`) + return + } + + // The admin public key + const adminPublicKey = await getPublicKeyByName(userState.accountName) + + // Create the raw remove transaction + const rawKickTransaction = await createGroupKickTransaction(minterAddress, adminPublicKey, 694, minterAddress) + + // Sign the transaction + const signedKickTransaction = await qortalRequest({ + action: "SIGN_TRANSACTION", + unsignedBytes: rawKickTransaction + }) + + // Process the transaction + const processResponse = await processTransaction(signedKickTransaction) + + if (processResponse?.status === "OK") { + alert(`${minterName}'s KICK transaction has been SUCCESSFULLY PROCESSED. Please WAIT FOR CONFIRMATION...`) } else { - return '' + alert("Failed to process the removal transaction.") } } catch (error) { - console.error('Error checking avatar availability:', error) - return '' + console.error("Error removing minter:", error) + alert("Error removing minter. Please try again.") } } +const handleBanMinter = async (minterName) => { + try { + + const { height: currentHeight } = await getLatestBlockInfo() + if (currentHeight <= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT) { + console.log(`block height is under the removal featureTrigger height`) + } + + const minterInfo = await getNameInfo(minterName) + const minterAddress = minterInfo?.owner + if (!minterAddress) { + alert(`No valid address found for minter name: ${minterName}`) + return + } + + const adminPublicKey = await getPublicKeyByName(userState.accountName) + + const rawBanTransaction = await createGroupBanTransaction(minterAddress, adminPublicKey, 694, minterAddress) + + const signedBanTransaction = await qortalRequest({ + action: "SIGN_TRANSACTION", + unsignedBytes: rawBanTransaction + }) + + const processResponse = await processTransaction(signedBanTransaction) + + if (processResponse?.status === "OK") { + alert(`${minterName}'s BAN transaction has been SUCCESSFULLY PROCESSED. Please WAIT FOR CONFIRMATION...`) + } else { + alert("Failed to process the removal transaction.") + } + + } catch (error) { + console.error("Error removing minter:", error) + alert("Error removing minter. Please try again.") + } +} // Create the overall Minter Card HTML ----------------------------------------------- const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => { @@ -1001,8 +1075,21 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co const minterGroupMembers = await fetchMinterGroupMembers() const minterAdmins = await fetchMinterGroupAdmins() const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier) + createModal('links') createModal('poll-details') + + let showRemoveHtml + const verifiedName = await validateMinterName(minterName) + if (verifiedName) { + console.log(`name is validated, utilizing for removal features...${verifiedName}`) + const removeActionsHtml = await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier) + showRemoveHtml = removeActionsHtml + } else { + console.log(`name could not be validated, assuming topic card (or some other issue with name validation) for removalActions`) + showRemoveHtml = '' + } + return `
@@ -1025,6 +1112,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co + ${showRemoveHtml}
Admin Support: ${adminYes} Admin Against: ${adminNo} diff --git a/assets/js/MinterBoard.js b/assets/js/MinterBoard.js index 5b036fc..f88fc4b 100644 --- a/assets/js/MinterBoard.js +++ b/assets/js/MinterBoard.js @@ -5,7 +5,7 @@ let isExistingCard = false let existingCardData = {} let existingCardIdentifier = {} const MIN_ADMIN_YES_VOTES = 9; -const MINTER_INVITE_BLOCK_HEIGHT = 9999999; // Example height, update later +const GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT = 99999999 //TODO update this to correct featureTrigger height when known, either that, or pull from core. let isApproved = false const loadMinterBoardPage = async () => { @@ -999,7 +999,7 @@ const generateDarkPastelBackgroundBy = (name) => { const handleInviteMinter = async (minterName) => { try { const blockInfo = await getLatestBlockInfo() - const blockHeight = toString(blockInfo.height) + const blockHeight = blockInfo.height if (blockHeight <= MINTER_INVITE_BLOCK_HEIGHT) { console.log(`block height is under the featureTrigger height`) } @@ -1020,7 +1020,7 @@ const handleInviteMinter = async (minterName) => { const processResponse = await processTransaction(signedTransaction) if (processResponse?.status === "OK") { - alert(`${minterName} has been successfully invited!`) + alert(`${minterName} has been successfully invited, please WAIT FOR CONFIRMATION...`) } else { alert("Failed to process the invite transaction.") } @@ -1046,9 +1046,9 @@ const createInviteButtonHtml = (creator, cardIdentifier) => { const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => { const latestBlockInfo = await getLatestBlockInfo() - const isBlockPassed = latestBlockInfo.height > MINTER_INVITE_BLOCK_HEIGHT + const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT - if (adminYes >= 9 && userState.isMinterAdmin) { + if (adminYes >= 9 && userState.isMinterAdmin && !isBlockPassed) { const inviteButtonHtml = createInviteButtonHtml(creator, cardIdentifier) console.log(`admin votes over 9, creating invite button...`, adminYes) return inviteButtonHtml @@ -1057,6 +1057,23 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => return null } +const getMinterAvatar = async (minterName) => { + const avatarUrl = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar` + try { + const response = await fetch(avatarUrl, { method: 'HEAD' }) + + if (response.ok) { + return `User Avatar` + } else { + return '' + } + + } catch (error) { + console.error('Error checking avatar availability:', error) + return '' + } +} + // Create the overall Minter Card HTML ----------------------------------------------- const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, BgColor) => { diff --git a/assets/js/QortalApi.js b/assets/js/QortalApi.js index 613386d..c45d927 100644 --- a/assets/js/QortalApi.js +++ b/assets/js/QortalApi.js @@ -1202,7 +1202,7 @@ const processTransaction = async (rawTransaction) => { // Create a group invite transaction. This will utilize a default timeToLive (which is how long the tx will be alive, not the time until it IS live...) of 10 days in seconds, as the legacy UI has a bug that doesn't display invites older than 10 days. // We will also default to the MINTER group for groupId, AFTER the GROUP_APPROVAL changes, the txGroupId will need to be set for tx that require approval. -const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive = 864000, txGroupId = 0) => { +const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive = 864000, txGroupId = 0, fee=0.01) => { try { // Fetch account reference correctly @@ -1217,7 +1217,7 @@ const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, gr const payload = { timestamp: Date.now(), reference: accountReference, - fee: 0.01, + fee: fee || 0.01, txGroupId: txGroupId, recipient: recipientAddress, adminPublicKey: adminPublicKey, @@ -1251,6 +1251,103 @@ const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, gr } } +const createGroupKickTransaction = async (recipientAddress, adminPublicKey, groupId=694, member, reason='Kicked by admins', txGroupId = 0, fee=0.01) => { + + try { + // Fetch account reference correctly + const accountInfo = await getAddressInfo(recipientAddress) + const accountReference = accountInfo.reference + + // Validate inputs before making the request + if (!adminPublicKey || !accountReference || !recipientAddress) { + throw new Error("Missing required parameters for group invite transaction.") + } + + const payload = { + timestamp: Date.now(), + reference: accountReference, + fee: fee | 0.01, + txGroupId: txGroupId, + recipient: recipientAddress, + adminPublicKey: adminPublicKey, + groupId: groupId, + member: member || recipientAddress, + reason: reason + } + + console.log("Sending group invite transaction payload:", payload) + + const response = await fetch(`${baseUrl}/groups/kick`, { + method: 'POST', + headers: { + 'Accept': 'text/plain', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to create transaction: ${response.status}, ${errorText}`) + } + + const rawTransaction = await response.text() + console.log("Raw unsigned transaction created:", rawTransaction) + return rawTransaction + } catch (error) { + console.error("Error creating group invite transaction:", error) + throw error + } +} + +const createGroupBanTransaction = async (recipientAddress, adminPublicKey, groupId=694, offender, reason='Banned by admins', txGroupId = 0, fee=0.01) => { + + try { + // Fetch account reference correctly + const accountInfo = await getAddressInfo(recipientAddress) + const accountReference = accountInfo.reference + + // Validate inputs before making the request + if (!adminPublicKey || !accountReference || !recipientAddress) { + throw new Error("Missing required parameters for group invite transaction.") + } + + const payload = { + timestamp: Date.now(), + reference: accountReference, + fee: fee | 0.01, + txGroupId: txGroupId, + recipient: recipientAddress, + adminPublicKey: adminPublicKey, + groupId: groupId, + offender: offender || recipientAddress, + reason: reason + } + + console.log("Sending group invite transaction payload:", payload) + + const response = await fetch(`${baseUrl}/groups/kick`, { + method: 'POST', + headers: { + 'Accept': 'text/plain', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to create transaction: ${response.status}, ${errorText}`) + } + + const rawTransaction = await response.text() + console.log("Raw unsigned transaction created:", rawTransaction) + return rawTransaction + } catch (error) { + console.error("Error creating group invite transaction:", error) + throw error + } +} const getLatestBlockInfo = async () => { try {