From a512b0ac7654d71b0b809fb77f688a039f020102 Mon Sep 17 00:00:00 2001 From: Hammad Jutt Date: Sat, 8 May 2021 16:12:18 -0600 Subject: [PATCH] Finalize setEthAddress command --- packages/@types/sourcecred/index.d.ts | 27 ++-- packages/discord-bot/package.json | 2 +- packages/discord-bot/src/config.ts | 10 +- .../discord-bot/src/discord/AppDiscord.ts | 3 +- .../discord-bot/src/discord/commands/getXp.ts | 148 ++++++++++-------- .../src/discord/commands/setEthAddress.ts | 26 ++- packages/discord-bot/src/index.ts | 2 +- packages/discord-bot/src/sourcecred.ts | 14 +- packages/discord-bot/src/start.ts | 8 +- 9 files changed, 139 insertions(+), 101 deletions(-) diff --git a/packages/@types/sourcecred/index.d.ts b/packages/@types/sourcecred/index.d.ts index 9ffbf235..69e713df 100644 --- a/packages/@types/sourcecred/index.d.ts +++ b/packages/@types/sourcecred/index.d.ts @@ -761,46 +761,45 @@ declare module 'sourcecred' { ledger: Ledger; persist: () => Promise; } - + export interface Ledger { accounts: () => SCAccountInfo[]; + account: (id: string) => SCAccountInfo; accountByAddress: (address: string) => SCAccountInfo; addAlias: (identityId: any, alias: any) => void; activate: (identityId: any) => void; } - + export interface ReloadResult { error: string | null; localChanges: any; } - + export interface SCReadInstance { readCredGraph: () => Promise; } - - export type CredGraph = { - - } - + + export type CredGraph = Record; + export interface SCAccountsData { accounts: SCAccount[]; intervalEndpoints: number[]; unclaimedAliases: SCUnclaimedAlias[]; } - + export interface SCAccount { account: SCAccountInfo; cred: number[]; totalCred: number; } - + export interface SCAccountInfo { active: boolean; balance: string; identity: SCIdentity; paid: string; } - + export interface SCIdentity { address: string; aliases: SCAlias[]; @@ -808,18 +807,18 @@ declare module 'sourcecred' { name: string; subtype: string; } - + export interface SCAlias { address: string; description: string; } - + export interface SCUnclaimedAlias { alias: SCAlias; cred: number[]; totalCred: number; } - + export interface AddressBookEntry { name: string; createdAt: number; diff --git a/packages/discord-bot/package.json b/packages/discord-bot/package.json index bbd92e54..ad50e01e 100644 --- a/packages/discord-bot/package.json +++ b/packages/discord-bot/package.json @@ -9,7 +9,7 @@ "start": "node ./dist/start.js", "build": "yarn generate && tsc -b", "dev": "concurrently \"yarn dev-ts\" \"yarn generate --watch\"", - "dev-ts": "ts-node-dev --exit-child --respawn -- src/index.ts", + "dev-ts": "ts-node-dev --exit-child --respawn -- src/start.ts", "typecheck": "yarn build", "precommit": "yarn lint-staged", "generate": "graphql-codegen --config=codegen.yml", diff --git a/packages/discord-bot/src/config.ts b/packages/discord-bot/src/config.ts index 096e7084..9dee1bd0 100644 --- a/packages/discord-bot/src/config.ts +++ b/packages/discord-bot/src/config.ts @@ -27,10 +27,7 @@ function parseEnv( export const CONFIG: IConfig = { port: parseEnv(process.env.PORT, 5000), graphqlURL: (() => { - const { - GRAPHQL_URL: url, - GRAPHQL_HOST: host, - } = process.env; + const { GRAPHQL_URL: url, GRAPHQL_HOST: host } = process.env; if (url) return url; if (host) { @@ -38,7 +35,10 @@ export const CONFIG: IConfig = { } return 'http://localhost:8080/v1/graphql'; })(), - adminKey: parseEnv(process.env.HASURA_GRAPHQL_ADMIN_SECRET, 'metagame_secret'), + adminKey: parseEnv( + process.env.HASURA_GRAPHQL_ADMIN_SECRET, + 'metagame_secret', + ), frontendUrl: parseEnv(process.env.FRONTEND_URL, 'http://localhost:3000'), githubApiToken: parseEnv(process.env.GITHUB_API_TOKEN, ''), discordBotToken: parseEnv(process.env.DISCORD_BOT_TOKEN, ''), diff --git a/packages/discord-bot/src/discord/AppDiscord.ts b/packages/discord-bot/src/discord/AppDiscord.ts index 2d0117b6..b780d226 100644 --- a/packages/discord-bot/src/discord/AppDiscord.ts +++ b/packages/discord-bot/src/discord/AppDiscord.ts @@ -4,7 +4,8 @@ import * as Path from 'path'; @Discord('', { import: [ // We are using tsc, so we want to load the compiled files - Path.join(__dirname, "commands", "*.js"), + Path.join(__dirname, 'commands', '*.ts'), + Path.join(__dirname, 'commands', '*.js'), ], }) export abstract class AppDiscord { diff --git a/packages/discord-bot/src/discord/commands/getXp.ts b/packages/discord-bot/src/discord/commands/getXp.ts index af8223f9..2cfc956e 100644 --- a/packages/discord-bot/src/discord/commands/getXp.ts +++ b/packages/discord-bot/src/discord/commands/getXp.ts @@ -1,10 +1,15 @@ -import { Constants } from "@metafam/utils"; -import { Command, CommandMessage } from "@typeit/discord"; -import { MessageEmbed, Snowflake } from "discord.js"; -import fetch from "node-fetch"; -import { SCAccount, SCAccountsData, SCAlias, sourcecred as sc } from "sourcecred"; +import { Constants } from '@metafam/utils'; +import { Command, CommandMessage } from '@typeit/discord'; +import { MessageEmbed, Snowflake } from 'discord.js'; +import fetch from 'node-fetch'; +import { + SCAccount, + SCAccountsData, + SCAlias, + sourcecred as sc, +} from 'sourcecred'; -import { getDiscordId, replyWithUnexpectedError } from "../../utils"; +import { getDiscordId, replyWithUnexpectedError } from '../../utils'; export class GetXpCommand { // todo rename to xp once previous bot is disabled @@ -14,16 +19,20 @@ export class GetXpCommand { try { if (message.args.discordUser) { targetUserDiscordId = getDiscordId(message.args.discordUser); - } else if (message.member?.id ) { + } else if (message.member?.id) { targetUserDiscordId = message.member.id; } } catch (e) { - await message.reply(`Could not recognize user ${message.args.discordUser}. Try \`!ac help\` if you need help.`); + await message.reply( + `Could not recognize user ${message.args.discordUser}. Try \`!ac help\` if you need help.`, + ); return; } if (targetUserDiscordId.trim().length === 0) { - await message.reply(`Could not recognize user. Try \`!ac help\` if you need help.`); + await message.reply( + `Could not recognize user. Try \`!ac help\` if you need help.`, + ); return; } @@ -34,60 +43,66 @@ export class GetXpCommand { await fetch(Constants.SC_ACCOUNTS_FILE) ).json(); - const scAccount = accountsData.accounts.find(account => filterAccount(account, targetUserDiscordId)); + const scAccount = accountsData.accounts.find((account) => + filterAccount(account, targetUserDiscordId), + ); if (scAccount != null) { const userTotalCred = scAccount.totalCred; const numWeeks = scAccount.cred.length; const userWeeklyCred = scAccount.cred; const variation = - (100 * (userWeeklyCred[numWeeks - 1] - userWeeklyCred[numWeeks - 2])) / + (100 * + (userWeeklyCred[numWeeks - 1] - userWeeklyCred[numWeeks - 2])) / userWeeklyCred[numWeeks - 2]; - const description = message.member?.id === targetUserDiscordId ? - `${discordUser}, here is your XP progression in MetaGame` : - `Here is the XP progression of ${discordUser} in MetaGame`; + const description = + message.member?.id === targetUserDiscordId + ? `${discordUser}, here is your XP progression in MetaGame` + : `Here is the XP progression of ${discordUser} in MetaGame`; - await message.reply(new MessageEmbed() - .setColor('#ff3864') - .setDescription(description) - .setTitle(`MetaGame XP`) - .setURL("https://xp.metagame.wtf/#/explorer") - .setTimestamp() - .setThumbnail( - 'https://raw.githubusercontent.com/sourcecred/sourcecred/master/src/assets/logo/rasterized/logo_64.png', - ) - .addFields( - { - name: 'Total', - value: `${Math.round(userTotalCred)} XP`, - inline: true, - }, - { - name: 'Last week ', - value: `${userWeeklyCred[numWeeks - 1].toPrecision(3)} XP`, - inline: true, - }, - { - name: 'Week before', - value: `${userWeeklyCred[numWeeks - 2].toPrecision(4)} XP`, - inline: true, - }, - { - name: 'Weekly Change', - value: `${variation.toPrecision(2)}%`, - inline: true, - }, - ) - .setFooter( - 'Bot made by MetaFam', - 'https://wiki.metagame.wtf/img/mg-crystal.png', - ), + await message.reply( + new MessageEmbed() + .setColor('#ff3864') + .setDescription(description) + .setTitle(`MetaGame XP`) + .setURL('https://xp.metagame.wtf/#/explorer') + .setTimestamp() + .setThumbnail( + 'https://raw.githubusercontent.com/sourcecred/sourcecred/master/src/assets/logo/rasterized/logo_64.png', + ) + .addFields( + { + name: 'Total', + value: `${Math.round(userTotalCred)} XP`, + inline: true, + }, + { + name: 'Last week ', + value: `${userWeeklyCred[numWeeks - 1].toPrecision(3)} XP`, + inline: true, + }, + { + name: 'Week before', + value: `${userWeeklyCred[numWeeks - 2].toPrecision(4)} XP`, + inline: true, + }, + { + name: 'Weekly Change', + value: `${variation.toPrecision(2)}%`, + inline: true, + }, + ) + .setFooter( + 'Bot made by MetaFam', + 'https://wiki.metagame.wtf/img/mg-crystal.png', + ), ); } else { - await message.reply(`I couldn't find ${discordUser} in the ledger! Have you registered yet?`) + await message.reply( + `I couldn't find ${discordUser} in the ledger! Have you registered yet?`, + ); } - } - catch (e) { + } catch (e) { await replyWithUnexpectedError(message); } } @@ -98,27 +113,32 @@ const filterAccount = (player: SCAccount, targetUserDiscordID: Snowflake) => { // Ignore if the target isn't a USER if (accountInfo.identity.subtype !== 'USER') return false; - const discordAliases = accountInfo.identity.aliases.filter( - alias => { - const parts = sc.core.graph.NodeAddress.toParts(alias.address); - return parts.indexOf('discord') > 0 - } - ); + const discordAliases = accountInfo.identity.aliases.filter((alias) => { + const parts = sc.core.graph.NodeAddress.toParts(alias.address); + return parts.indexOf('discord') > 0; + }); if (discordAliases.length >= 1) { // Retrieve the Discord ID - const discordId = discordAliases.find(discordAccount => scAliasMatchesDiscordId(discordAccount, targetUserDiscordID)); - if (discordId !== undefined){ + const discordId = discordAliases.find((discordAccount) => + scAliasMatchesDiscordId(discordAccount, targetUserDiscordID), + ); + if (discordId !== undefined) { return accountInfo; } } return false; -} +}; -const scAliasMatchesDiscordId = (discordAccount: SCAlias, targetUserDiscordID: Snowflake) => { - const discordId = sc.core.graph.NodeAddress.toParts(discordAccount.address)[4]; +const scAliasMatchesDiscordId = ( + discordAccount: SCAlias, + targetUserDiscordID: Snowflake, +) => { + const discordId = sc.core.graph.NodeAddress.toParts( + discordAccount.address, + )[4]; if (discordId === targetUserDiscordID) { return discordId; } return undefined; -} +}; diff --git a/packages/discord-bot/src/discord/commands/setEthAddress.ts b/packages/discord-bot/src/discord/commands/setEthAddress.ts index e97352af..0a5ed51f 100644 --- a/packages/discord-bot/src/discord/commands/setEthAddress.ts +++ b/packages/discord-bot/src/discord/commands/setEthAddress.ts @@ -1,12 +1,12 @@ -import { CommandMessage } from "@typeit/discord"; -import { ReloadResult, sourcecred as sc } from "sourcecred"; +import { Command, CommandMessage } from '@typeit/discord'; +import { sourcecred as sc } from 'sourcecred'; -import { loadSourceCredLedger, manager } from "../../sourcecred"; +import { loadSourceCredLedger, manager } from '../../sourcecred'; const addressUtils = sc.plugins.ethereum.utils.address; export abstract class SetEthAddress { - // @Command('setAddress :ethAddress') + @Command('!setAddress :ethAddress') async setAddress(message: CommandMessage) { const res = await loadSourceCredLedger(); @@ -23,7 +23,7 @@ export abstract class SetEthAddress { baseIdentityProposal, ); - let ethAddress; + let ethAddress: string; try { ethAddress = addressUtils.parseAddress(message.args.ethAddress); } catch (e) { @@ -45,10 +45,24 @@ export abstract class SetEthAddress { return; } + const account = manager.ledger.account(baseIdentityId); + + const existing = account.identity.aliases.find((alias) => { + const parts = sc.core.graph.NodeAddress.toParts(alias.address); + return parts.indexOf('ethereum') > 0; + }); + + if (existing) { + await message.reply( + `You already have linked the following ETH Address: \`${existing.description}\`.`, + ); + return; + } + try { manager.ledger.addAlias(baseIdentityId, ethAlias); manager.ledger.activate(baseIdentityId); - const persistRes: ReloadResult = await manager.persist(); + const persistRes = await manager.persist(); if (persistRes.error) { await message.reply( diff --git a/packages/discord-bot/src/index.ts b/packages/discord-bot/src/index.ts index 71a02f1e..0b791ffc 100644 --- a/packages/discord-bot/src/index.ts +++ b/packages/discord-bot/src/index.ts @@ -7,6 +7,7 @@ async function createDiscordClient(): Promise { const client = new Client({ classes: [ // We are using tsc, so we want to load the compiled files + `${__dirname}/discord/**/*.ts`, // glob string to load the classes `${__dirname}/discord/**/*.js`, // glob string to load the classes ], silent: false, @@ -14,7 +15,6 @@ async function createDiscordClient(): Promise { }); await client.login(CONFIG.discordBotToken); - return client; } diff --git a/packages/discord-bot/src/sourcecred.ts b/packages/discord-bot/src/sourcecred.ts index 65d267aa..877a80f8 100644 --- a/packages/discord-bot/src/sourcecred.ts +++ b/packages/discord-bot/src/sourcecred.ts @@ -1,4 +1,4 @@ -import { LedgerManager, ReloadResult, sourcecred } from "sourcecred"; +import { LedgerManager, ReloadResult, sourcecred } from 'sourcecred'; import { CONFIG } from './config'; @@ -8,9 +8,11 @@ const storage = new sourcecred.ledger.storage.GithubStorage({ branch: 'master', }); -export const manager: LedgerManager = new sourcecred.ledger.manager.LedgerManager({ - storage, -}); +export const manager: LedgerManager = new sourcecred.ledger.manager.LedgerManager( + { + storage, + }, +); let loading = false; let ledgerLoadedPromise: Promise; @@ -19,7 +21,7 @@ export const loadSourceCredLedger = (): Promise => { if (ledgerLoadedPromise == null) { if (!loading) { loading = true; - console.log('reloading ledger...') + console.log('reloading ledger...'); ledgerLoadedPromise = manager.reloadLedger(); ledgerLoadedPromise.then(() => { loading = false; @@ -28,4 +30,4 @@ export const loadSourceCredLedger = (): Promise => { } return ledgerLoadedPromise; -} +}; diff --git a/packages/discord-bot/src/start.ts b/packages/discord-bot/src/start.ts index d4391d5e..4e8de0d1 100644 --- a/packages/discord-bot/src/start.ts +++ b/packages/discord-bot/src/start.ts @@ -1,9 +1,9 @@ import express from 'express'; -import { createDiscordClient } from "."; +import { createDiscordClient } from '.'; import { CONFIG } from './config'; -createDiscordClient(); +const discordClientPromise = createDiscordClient(); const app = express(); @@ -12,5 +12,7 @@ app.get('/healthz', (_, res) => { }); app.listen(CONFIG.port, () => { - console.log(`Discord bot started on port ${CONFIG.port}`); + discordClientPromise.then(() => { + console.log(`Discord bot started on port ${CONFIG.port}`); + }); });