-
Notifications
You must be signed in to change notification settings - Fork 1
feat(subm): 1 BLD request status google sheet #44
base: main
Are you sure you want to change the base?
Changes from all commits
a61de7c
fca01c6
9110ff7
411f5ac
7db34e2
4dde767
243a315
3456a84
2ccee05
e41ea27
ca26fb2
6c18f04
53a31a7
b88d815
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
/* eslint-disable no-await-in-loop */ | ||
// See https://github.com/Agoric/validator-profiles/wiki/Request-1-BLD | ||
|
||
const { DiscordAPI, getContent, paged } = require('./discordGuild.js'); | ||
const { upsert } = require('./sheetAccess'); | ||
const { searchBySender, transfers } = require('./tendermintRPC'); | ||
|
||
const config = { | ||
host: 'rpc-agoric.nodes.guru', | ||
address: 'agoric15qxmfufeyj4zm9zwnsczp72elxsjsvd0vm4q8h', | ||
quorum: 2, | ||
}; | ||
|
||
const fail = () => { | ||
throw Error(); | ||
}; | ||
|
||
/** | ||
* @param {ReturnType<ReturnType<typeof DiscordAPI>['channels']>} channel | ||
* @param {ReturnType<ReturnType<typeof DiscordAPI>['guilds']>} guild | ||
* @param {Snowflake} role | ||
* @param {number} quorum | ||
* @yields {{ message: Message, endorsers: User[] }} | ||
* @typedef {import('./discordGuild').Snowflake} Snowflake | ||
*/ | ||
async function* authorizedRequests(channel, guild, role, quorum) { | ||
/** @type {Map<Snowflake, import('./discordGuild').GuildMember>} */ | ||
const memberDetail = new Map(); | ||
/** @param {Snowflake} id */ | ||
const getMemberDetail = async id => { | ||
if (memberDetail.has(id)) { | ||
return memberDetail.get(id) || fail(); | ||
} | ||
const detail = await guild.members(id); | ||
// console.log(detail); | ||
memberDetail.set(id, detail); | ||
return detail; | ||
}; | ||
|
||
const messages = await paged(channel.getMessages); | ||
const hasAddr = messages.filter(msg => msg.content.match(/agoric1/)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. need tighter regex match There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. need? really? I think no regex is needed at all; this is just an optimization to avoid processing most messages. The 2 reviewers are the real gating factor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. true as the extract is lower - i figured it would be possible to copy the same regex up here to tighten down the search |
||
if (!hasAddr) return; | ||
const hasChecks = hasAddr.filter(msg => { | ||
const [checks] = (msg.reactions || []).filter(r => r.emoji.name === '✅'); | ||
return (checks || {}).count >= quorum; | ||
}); | ||
if (!hasChecks) return; | ||
|
||
for (const msg of hasChecks) { | ||
const endorsements = await channel.messages(msg.id).reactions('✅'); | ||
const endorsers = []; | ||
for (const endorsement of endorsements) { | ||
const detail = await getMemberDetail(endorsement.id); | ||
if (detail && detail.roles && detail.roles.includes(role)) { | ||
endorsers.push(detail); | ||
} | ||
} | ||
if (endorsers.length >= quorum) { | ||
const [_, address] = msg.content.match(/(agoric1\S+)/); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. need tighter regex match + bech32 validation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's not clear to me why this is needed either. For a correct request, this is sufficient to pick out the address. This tool isn't designed to validate requests. It's designed to find requests that have been validated by reviewers. I guess I can read "need" as "please add ...". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we're operating on the value - so this would allow an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. operating on the value is good justification. thanks. |
||
if (typeof address !== 'string') throw TypeError(address); | ||
yield { message: msg, address, endorsers }; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @param {ReturnType<ReturnType<typeof DiscordAPI>['channels']>} channel | ||
* @param {ReturnType<ReturnType<typeof DiscordAPI>['guilds']>} guild | ||
* @param {string} roleID | ||
* @param {{ | ||
* get: typeof import('https').get, | ||
* }} io | ||
*/ | ||
async function requestStatus(channel, guild, roleID, { get }) { | ||
const txs = await getContent( | ||
config.host, | ||
searchBySender(config.address), | ||
{}, | ||
{ get }, | ||
).then(txt => JSON.parse(txt).result.txs); | ||
|
||
const txfrs = transfers(txs); | ||
// console.log(txfrs); | ||
const byRecipient = new Map(txfrs.map(txfr => [txfr.recipient, txfr])); | ||
// console.log(byRecipient.keys()); | ||
|
||
const result = []; | ||
const eachRequest = authorizedRequests(channel, guild, roleID, config.quorum); | ||
for await (const { message, address, endorsers } of eachRequest) { | ||
const hash = byRecipient.has(address) | ||
? byRecipient.get(address).hash | ||
: undefined; | ||
result.push({ | ||
message, | ||
address, | ||
endorsers, | ||
hash, | ||
}); | ||
} | ||
return result; | ||
} | ||
|
||
const label = user => `${user.username}#${user.discriminator}`; | ||
|
||
/** | ||
* @param {Record<string, string | undefined>} env | ||
* @param {{ | ||
* get: typeof import('https').get, | ||
* setTimeout: typeof setTimeout, | ||
* }} io | ||
*/ | ||
async function main(env, { get, setTimeout }) { | ||
const discordAPI = DiscordAPI(env.DISCORD_API_TOKEN, { get, setTimeout }); | ||
const guild = discordAPI.guilds(env.DISCORD_GUILD_ID); | ||
|
||
// to get mod-1-bld role id: | ||
// console.log(await guild.roles()); | ||
|
||
const channel = discordAPI.channels(env.CHANNEL_ID); | ||
|
||
const txs = await getContent( | ||
config.host, | ||
searchBySender(config.address), | ||
{}, | ||
{ get }, | ||
).then(txt => JSON.parse(txt).result.txs); | ||
|
||
const txfrs = transfers(txs); | ||
// console.log(txfrs); | ||
const byRecipient = new Map(txfrs.map(txfr => [txfr.recipient, txfr])); | ||
// console.log(byRecipient.keys()); | ||
|
||
const header = [ | ||
'timestamp', | ||
'msgID', | ||
'requestor', | ||
'address', | ||
'endorsers', | ||
'hash', | ||
]; | ||
console.log(header.join(',')); | ||
for await (const { message: msg, address, endorsers } of authorizedRequests( | ||
channel, | ||
guild, | ||
env.REVIEWER_ROLE_ID, | ||
2, | ||
)) { | ||
const ok = endorsers.map(u => label(u.user)).join(' '); | ||
const hash = byRecipient.has(address) ? byRecipient.get(address).hash : ''; | ||
console.log( | ||
`${msg.timestamp},${msg.id},${label( | ||
msg.author, | ||
)},${address},${ok},${hash}`, | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* @param {string[]} args | ||
* @param {Record<string, string | undefined>} env | ||
* @param {Object} io | ||
* @param {typeof import('google-spreadsheet').GoogleSpreadsheet} io.GoogleSpreadsheet | ||
* @param {typeof import('https').get} io.get | ||
* @param {typeof setTimeout} io.setTimeout | ||
*/ | ||
const main2 = async (args, env, { get, setTimeout, GoogleSpreadsheet }) => { | ||
const discordAPI = DiscordAPI(env.DISCORD_API_TOKEN, { get, setTimeout }); | ||
const guild = discordAPI.guilds(env.DISCORD_GUILD_ID); | ||
|
||
// to get mod-1-bld role id: | ||
// console.log(await guild.roles()); | ||
|
||
const channel = discordAPI.channels(env.CHANNEL_ID); | ||
|
||
const creds = { | ||
client_email: env.GOOGLE_SERVICES_EMAIL, | ||
private_key: env.GCS_PRIVATE_KEY, | ||
}; | ||
// Initialize the sheet - doc ID is the long id in the sheets URL | ||
const doc = new GoogleSpreadsheet(env.SHEET_1BLD_ID); | ||
// Initialize Auth - see https://theoephraim.github.io/node-google-spreadsheet/#/getting-started/authentication | ||
await doc.useServiceAccountAuth(creds); | ||
await doc.loadInfo(); // loads document properties and worksheets | ||
console.log(doc.title); | ||
const sheet = doc.sheetsByIndex[0]; | ||
|
||
const memberLabel = mem => mem.nick || label(mem.user); | ||
const eachStatus = await requestStatus(channel, guild, env.REVIEWER_ROLE_ID, { | ||
get, | ||
}); | ||
for (const { message: msg, address, endorsers, hash } of eachStatus) { | ||
const record = { | ||
To: address, | ||
Link: `https://discord.com/channels/${env.DISCORD_GUILD_ID}/${env.CHANNEL_ID}/${msg.id}`, | ||
At: msg.timestamp.slice(0, '1999-01-01T12:59'.length).replace('T', ' '), | ||
By: label(msg.author), | ||
Reviewers: endorsers.map(memberLabel).join(','), | ||
Tx: hash && `https://agoric.bigdipper.live/transactions/${hash}`, | ||
Request: msg.content, | ||
}; | ||
await upsert(sheet, address, record); // do not interleave upserts! | ||
} | ||
}; | ||
|
||
/* global require, process, module */ | ||
if (require.main === module) { | ||
main2( | ||
process.argv.slice(2), | ||
{ ...process.env }, | ||
{ | ||
// eslint-disable-next-line global-require | ||
get: require('https').get, | ||
setTimeout, | ||
// eslint-disable-next-line global-require | ||
GoogleSpreadsheet: require('google-spreadsheet').GoogleSpreadsheet, // please excuse CJS | ||
}, | ||
).catch(err => console.error(err)); | ||
} | ||
|
||
module.exports = { requestStatus }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these freezes would ideally be hardens. I don't know how helpful they are since they are not recursive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a certain level of PTSD from trying to use HardenedJS in CLI tools. Perhaps it's less painful now than when I ran into problems a while ago. I can give it a try.