Browse Source

faucet: old version (emulate browser) + new vesrion (api)

agrafen 3 năm trước cách đây
mục cha
commit
1b70188e77

+ 2 - 0
community-contributions/joystream-leaderboard-bot/.gitignore

@@ -25,6 +25,8 @@ yarn-error.log*
 
 fmInfo.json
 
+screenshots
+accounts/*
 
 
 # #############################

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 994 - 499
community-contributions/joystream-leaderboard-bot/package-lock.json


+ 29 - 1
community-contributions/joystream-leaderboard-bot/package.json

@@ -1,6 +1,6 @@
 {
   "name": "joystream-leaderboard-bot",
-  "version": "0.2.0",
+  "version": "0.3.0",
   "description": "",
   "main": "index.js",
   "scripts": {
@@ -9,12 +9,40 @@
   "author": "agrafen",
   "license": "ISC",
   "dependencies": {
+    "@joystream/content-metadata-protobuf": "^1.1.0",
+    "@joystream/types": "^0.16.1",
+    "@polkadot/api": "4.2.1",
+    "@types/cli-progress": "^3.9.1",
+    "@types/fluent-ffmpeg": "^2.1.16",
+    "@types/inquirer": "^6.5.0",
+    "@types/mime-types": "^2.1.0",
+    "@types/proper-lockfile": "^4.1.1",
+    "@types/slug": "^0.9.1",
+    "ajv": "^6.11.0",
     "axios": "^0.21.1",
     "bluebird": "^3.7.2",
+    "bn.js": "^5.2.0",
+    "chalk": "^4.1.2",
+    "cli-progress": "^3.9.0",
+    "cli-ux": "^5.4.5",
     "discord.js": "^12.5.3",
     "dotenv": "^10.0.0",
+    "fluent-ffmpeg": "^2.1.2",
+    "inquirer": "^7.1.0",
+    "inquirer-datepicker-prompt": "^0.4.2",
+    "ipfs-http-client": "^47.0.1",
+    "ipfs-only-hash": "^1.0.2",
+    "it-all": "^1.0.4",
+    "it-drain": "^1.0.3",
+    "it-first": "^1.0.4",
+    "it-last": "^1.0.4",
+    "it-to-buffer": "^1.0.4",
+    "json-schema": "^0.2.3",
     "mongoose": "5.9.25",
     "node-telegram-bot-api": "^0.53.0",
+    "puppeteer": "^10.2.0",
+    "shelljs": "^0.8.4",
+    "slug": "^5.1.0",
     "ts-node": "^10.1.0",
     "tsconfig-paths": "^3.10.1",
     "typescript": "^4.3.5"

+ 340 - 33
community-contributions/joystream-leaderboard-bot/src/botService.ts

@@ -1,28 +1,12 @@
-import { MemberModel, IMember } from './db';
+import { MemberModel, IMember, FaucetModel } from './db';
 import axios from 'axios';
+import puppeteer from 'puppeteer';
+import TelegramBot from 'node-telegram-bot-api';
 
-type SendMessage = (message: any, text: string) => Promise<any>;
-type CommandPrefix = '/' | '!' | string;
-
-interface BotServiceProps {
-  send: SendMessage;
-  commandPrefix: CommandPrefix;
-  client: any; // TelegramBot | DiscordBot,
-  getId: Function;
-  getText: Function;
-  getDate: Function;
-  dbId: 'tgId' | 'disId' | string;
-  log: Function;
-}
+import { SendMessage, CommandPrefix, BotServiceProps } from './types';
+import AccountTransferTokens from './commands/transferTokens';
 
-interface BotProps {
-  send: SendMessage;
-  commandPrefix: CommandPrefix;
-  client: any;
-  id: number;
-  text: string;
-  dbId: 'tgId' | 'disId' | string;
-}
+const prod = process.env.PRODUCTION === 'true';
 
 let cachedFmInfo: any = null;
 let lastUpdateFmDate = 0;
@@ -57,7 +41,7 @@ function startCommand(message: any, props: BotServiceProps) {
 }
 
 async function lookupCommand(
-  member: IMember,
+  member: IMember | null,
   message: any,
   props: BotServiceProps
 ) {
@@ -67,7 +51,7 @@ async function lookupCommand(
   if (regexp.test(props.getText(message))) {
     const match = props.getText(message).match(regexpMatch);
     console.log('message', message);
-    
+
     const handle = match ? match[1] : member?.handle;
 
     if (handle) {
@@ -78,11 +62,14 @@ async function lookupCommand(
 
       if (memberData) {
         const memberDataStr =
+          `From data processed berofe: ${new Date(
+            fmInfo.data.scores.cutoff
+          ).toLocaleDateString()}\n\n` +
           `Direct Score = *${memberData.totalDirectScore}*\n` +
           `Referral Score = *${memberData.totalReferralScore}*\n` +
           `Total Score = *${memberData.totalScore}*\n\n` +
           memberData.directScores
-            .map((m: any, index: number) => `Period ${index} = *${m}*`)
+            .map((m: any, index: number) => `Period ${index} = *${m || 0}*`)
             .join('\n');
 
         props.send(message, memberDataStr);
@@ -99,7 +86,7 @@ async function lookupCommand(
 }
 
 async function setHandleCommand(
-  member: IMember,
+  member: IMember | null,
   message: any,
   props: BotServiceProps
 ) {
@@ -133,12 +120,12 @@ async function setHandleCommand(
     if (updMember) {
       await MemberModel.remove({
         [props.dbId]: props.getId(message),
-        handle: null,
+        handle: undefined, // TODO !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
       });
 
       await updMember.updateOne({
         $set: {
-          lastCommand: null,
+          lastCommand: undefined,
           [props.dbId]: props.getId(message),
           handle: props.getText(message),
         },
@@ -146,7 +133,7 @@ async function setHandleCommand(
     } else {
       await MemberModel.updateOne(
         { [props.dbId]: props.getId(message) },
-        { $set: { lastCommand: null, handle: props.getText(message) } }
+        { $set: { lastCommand: undefined, handle: props.getText(message) } }
       );
     }
 
@@ -154,6 +141,318 @@ async function setHandleCommand(
   }
 }
 
+async function faucetOldVersion(memberAddress = '', sendMessage: Function) {
+  const browser = await puppeteer.launch({
+    args: ['--lang="en-US"'],
+  });
+  const page = await browser.newPage();
+  await page.setExtraHTTPHeaders({
+    'Accept-Language': 'en',
+  });
+
+  await page.goto('https://testnet.joystream.org/#/accounts');
+  await page.waitForTimeout(5000);
+
+  sendMessage('Preparing for transfer...');
+
+  // Open restore json window
+  const restoreJSONText = prod ? 'Restore JSON' : 'Восстановить из JSON файла';
+  const [restoreJSONButton] = await page.$x(
+    `//button[contains(., '${restoreJSONText}')]`
+  );
+  await restoreJSONButton.click();
+
+  // Fill fields (file + password)
+  // const clickToSelectText = prod ? 'click to select or drag and drop the file here' : 'нажмите для выбора, либо сбростье файл сюда';
+  await page.click('.ui--InputFile');
+  const inputFile = await page.$('.ui--InputFile input[type="file"]');
+  // const inputPassword = await page.$('.ui--Input input[type="password"]');
+  await page.type(
+    '.ui--Input input[type="password"]',
+    process.env.ACCOUNT_PASSWORD || ''
+  );
+  await inputFile?.uploadFile(`accounts/${process.env.ACCOUNT_JSON_NAME}` || '');
+
+  // Click restore button
+  const restoreButtonText = prod ? 'Restore' : 'Восстановить'; // 'Отменить'; //'Восстановить';
+  const restoreButtons = await page.$x(
+    `//button[contains(., '${restoreButtonText}')]`
+  );
+
+  await restoreButtons[1].click();
+
+  sendMessage('Preparing for transfer - step 2...');
+  // Click send button
+  const sendButtonText = prod ? 'send' : 'отправить';
+  const [sendButton] = await page.$x(
+    `//button[contains(., '${sendButtonText}')]`
+  );
+  await sendButton.click();
+
+  // Fill address
+  const sendToAddressSelectText = prod
+    ? 'send to address'
+    : 'отправить на адрес';
+  const sendAmountText = prod ? 'amount' : 'сумма';
+
+  const [addresSelector] = await page.$x(
+    `//div[contains(@class, "ui--Modal-Column") and contains(., "${sendToAddressSelectText}")]/div[contains(@class, "ui--Modal-Column")]//input`
+  );
+
+  const [labelAmount] = await page.$x(
+    `//div[contains(@class, "ui--Modal-Column") and contains(., "${sendAmountText}")]/div[contains(@class, "ui--Modal-Column")]//input`
+  );
+
+  console.log('memberAddress', memberAddress);
+
+  await page.screenshot({ path: `screenshots/send.png` });
+  await labelAmount.click({ clickCount: 3 });
+  await labelAmount.type('1' /* '101' */);
+  await page.screenshot({ path: `screenshots/send1.png` });
+  await addresSelector.type(memberAddress);
+  await page.screenshot({ path: `screenshots/send2.png` });
+
+  sendMessage('Preparing for transfer - step 3...');
+
+  // Click to transfer & Sign and Submit
+  const transferText = prod ? 'Make Transfer' : 'Выполнить Трансфер';
+  const [transferButton] = await page.$x(
+    `//button[contains(., '${transferText}')]`
+  );
+  await transferButton.click();
+
+  const unlockAccountText = prod
+    ? 'unlock account with password'
+    : 'разблокировать аккаунт с помощью пароля';
+
+  const signAndSubmitText = prod ? 'Sign and Submit' : 'Подписать и отправить';
+
+  const [unlockPasswordInput] = await page.$x(
+    `//div[contains(@class, "ui--Modal-Column") and contains(., "${unlockAccountText}")]/div[contains(@class, "ui--Modal-Column")]//input`
+  );
+
+  const [signAndSubmitButton] = await page.$x(
+    `//button[contains(., '${signAndSubmitText}')]`
+  );
+
+  sendMessage('Start transfer tokens');
+
+  await unlockPasswordInput.type(process.env.ACCOUNT_PASSWORD || '');
+  await signAndSubmitButton.click();
+  await page.waitForTimeout(2000);
+
+  console.log('-------------------- create screenshot --------------------');
+
+  await page.screenshot({ path: `screenshots/example.png` });
+  await page.screenshot({ path: `screenshots/wallet-${memberAddress}.png` });
+
+  console.log('-------------------- finish --------------------');
+
+  await browser.close();
+}
+
+async function setLastCommand(
+  member: IMember | null,
+  message: any,
+  props: BotServiceProps,
+  lastCommand: string
+) {
+  if (member === null) {
+    const newMember = {
+      [props.dbId]: props.getId(message),
+      date: props.getDate(message),
+      lastCommand,
+    };
+
+    await MemberModel.create(newMember);
+  } else {
+    await MemberModel.updateOne(
+      { [props.dbId]: props.getId(message) },
+      { $set: { lastCommand } }
+    );
+  }
+}
+
+async function faucetCommand(
+  member: IMember | null,
+  message: any,
+  props: BotServiceProps
+) {
+  const id = props.getId(message);
+  const regexp = new RegExp(`^${props.commandPrefix}faucet(.*)`);
+  const faucetMemberData = await FaucetModel.findOne({ [props.dbId]: id });
+  const dateLastOperation = faucetMemberData?.dateLastOperation || 0;
+
+  const currentMessage = props.getText(message);
+
+  const faucetPeriod =
+    60000 *
+    10 *
+    (faucetMemberData?.addresses.length >= 3
+      ? (faucetMemberData?.addresses.length - 2) * 60
+      : 1);
+
+  if (regexp.test(currentMessage)) {
+    // /faucet
+
+    await setLastCommand(member, message, props, 'faucet');
+
+    if (faucetMemberData === null) {
+      const newFaucet = {
+        [props.dbId]: props.getId(message),
+        date: props.getDate(message),
+        dateUpdate: props.getDate(message),
+      };
+
+      await FaucetModel.create(newFaucet);
+    } else if (
+      new Date().getTime() - dateLastOperation < faucetPeriod
+    ) {
+      await setLastCommand(member, message, props, '');
+      return await props.send(message, 'You can use the bot no more then once every 10 minutes');
+    }
+
+    return await props.send(
+      message,
+      'Send your wallet address. To cancel the operation, send Q.'
+    );
+  } else if (
+    member &&
+    member.lastCommand === 'faucet' &&
+    currentMessage.toLowerCase() === 'q'
+  ) {
+    // Cancel
+    await setLastCommand(member, message, props, '');
+    await props.send(message, 'Canceled transfer');
+  } else if (
+    member &&
+    member.lastCommand === 'faucet' &&
+    // && !faucetMemberData?.address
+    !faucetMemberData?.addresses.includes(currentMessage) &&
+    new Date().getTime() - dateLastOperation > faucetPeriod
+  ) {
+    // Transfer
+    props.send(message, 'Wait a few minutes...');
+
+    const checkAddress = await FaucetModel.find({ addresses: { $all: [currentMessage] } });
+
+    console.log('checkAddress', checkAddress);
+
+    if (checkAddress.length > 0) {
+      props.send(message, 'Transfer has already been made to this address');
+      throw new Error('Error: Transfer has already been made to this address');
+    }
+
+    await setLastCommand(member, message, props, '');
+
+    try {
+      await faucet(currentMessage, (text: string) => props.send(message, text));
+
+      await props.send(message, 'You got funded!');
+
+      await FaucetModel.updateOne(
+        { [props.dbId]: props.getId(message) },
+        { $set: { dateLastOperation: new Date().getTime(), addresses: [...faucetMemberData?.addresses, currentMessage] } }
+      );
+    } catch (ex) {
+      console.error(ex);
+      props.send(message, ex.message);
+    }
+
+    const transferTokens = new AccountTransferTokens();
+    const balance = await transferTokens.getBalance();
+    if (balance.toBigInt() < 1000) {
+      // send notification only once a day
+      const dateLastNotify = (new Date().getTime()) - 1000 * 60 * 60 * 24;
+      const notifyMembers = await MemberModel.find({ enableNotify: { $lt: dateLastNotify } });
+
+      notifyMembers.forEach(async (m) => {
+        await props.send({ chat: { id: m.tgId } }, `Bot balance alert! Current balance - ${balance}`);
+
+        await MemberModel.updateOne(
+          { tgId: m.tgId },
+          { $set: { lastCommand: '', enableNotify: (new Date().getTime()) } }
+        );
+      });
+    }
+
+    return props.send(message, 'Operation completed.');
+  } else if (
+    member &&
+    member.lastCommand === 'faucet' &&
+    (faucetMemberData?.addresses.includes(currentMessage) ||
+      new Date().getTime() - dateLastOperation < faucetPeriod)
+  ) {
+    // Reject
+    await setLastCommand(member, message, props, '');
+    return props.send(
+      message,
+      /* 'Can only be done once a day.' */ 'Transfer has already been made to this address'
+    );
+  }
+}
+
+async function faucet(
+  walletAddress = '5HfxszoqKG9MPxp1WfywYAynUaTnSfmguCidpSGWJvqwaPpu',
+  sendMessage: Function
+) {
+  const transferTokens = new AccountTransferTokens();
+  await transferTokens.start(walletAddress, prod ? 101 : 1);
+}
+
+async function checkFaucetBalanceCommand(message: any, props: BotServiceProps) {
+  const regexp = new RegExp(`^${props.commandPrefix}balancefaucet`);
+
+  if (regexp.test(props.getText(message))) {
+    const transferTokens = new AccountTransferTokens();
+    const balance = await transferTokens.getBalance();
+    console.log('balance', balance);
+
+    props.send(message, `Balance = ${balance}`);
+  }
+}
+
+// enable notification about expiring balance
+async function enableFaucetBalanceNotifyTgCommand(
+  member: IMember | null,
+  message: TelegramBot.Message,
+  props: BotServiceProps
+) {
+  changeFaucetBalanceNotifyTg(member, message, props, 'enablefaucetnotify', true);
+}
+
+async function disableFaucetBalanceNotifyTgCommand(
+  member: IMember | null,
+  message: TelegramBot.Message,
+  props: BotServiceProps
+) {
+  changeFaucetBalanceNotifyTg(member, message, props, 'disablefaucetnotify', false);
+}
+
+async function changeFaucetBalanceNotifyTg(
+  member: IMember | null,
+  message: TelegramBot.Message,
+  props: BotServiceProps,
+  command = '',
+  enableNotify = true,
+) {
+  const regexp = new RegExp(`^${props.commandPrefix}${command}`);
+  if (regexp.test(props.getText(message))) {
+    if (member) {
+      await MemberModel.updateOne(
+        { [props.dbId]: props.getId(message) },
+        { $set: { lastCommand: '', enableNotify: enableNotify ? (new Date().getTime() - 1000 * 60 * 60 * 24) : undefined } }
+      );
+
+      console.log('message', message);
+      
+      props.send(message, `Notifies ${enableNotify ? 'enabled' : 'disabled'}`);
+    } else {
+      props.send(message, 'Set your handle througs /sethandle');
+    }
+  }
+}
+
 export default async function BotService(props: BotServiceProps) {
   props.client.on('message', async (message: any) => {
     const id = props.getId(message);
@@ -167,11 +466,19 @@ export default async function BotService(props: BotServiceProps) {
       props.send(message, 'Error =( please try later');
     }
 
-    startCommand(message, props);
-    lookupCommand(member, message, props);
+    try {
+      startCommand(message, props);
+      lookupCommand(member, message, props);
 
-    if (props.commandPrefix === '/') {
-      setHandleCommand(member, message, props);
+      if (props.commandPrefix === '/') {
+        setHandleCommand(member, message, props);
+        faucetCommand(member, message, props);
+        checkFaucetBalanceCommand(message, props);
+        enableFaucetBalanceNotifyTgCommand(member, message, props);
+        disableFaucetBalanceNotifyTgCommand(member, message, props);
+      }
+    } catch (e) {
+      await props.send(message, e);
     }
   });
 }

+ 0 - 0
community-contributions/joystream-leaderboard-bot/src/commands/start.ts


+ 199 - 0
community-contributions/joystream-leaderboard-bot/src/commands/transferTokens.ts

@@ -0,0 +1,199 @@
+/*
+  This is adapted code from CLI
+*/
+import BN from 'bn.js';
+import { Keyring } from '@polkadot/api';
+import { ApiPromise, WsProvider } from '@polkadot/api';
+import { DeriveBalancesAll } from '@polkadot/api-derive/types';
+import {
+  SubmittableExtrinsic,
+} from '@polkadot/api/types';
+import { types } from '@joystream/types';
+import { Balance } from '@polkadot/types/interfaces';
+import { KeyringPair } from '@polkadot/keyring/types';
+import chalk from 'chalk';
+import { formatBalance } from '@polkadot/util';
+import { Hash } from '@polkadot/types/interfaces';
+
+import { checkBalance, validateAddress } from './validation';
+
+export const DEFAULT_API_URI = 'wss://rome-rpc-endpoint.joystream.org:9944/';
+
+type NamedKeyringPair = KeyringPair & {
+  meta: {
+    name: string
+  }
+}
+
+export default class TransferTokens {
+  private api: ApiPromise;
+
+  getAccountBalancesInfo = async (
+    accountAddresses: string
+  ): Promise<DeriveBalancesAll> => {
+    return await Promise.resolve(
+      this.api.derive.balances.all(accountAddresses)
+    );
+  };
+
+  createTransferTx(recipient: string, amount: BN) {
+    return this.api.tx.balances.transfer(recipient, amount);
+  }
+
+  async estimateFee(
+    account: KeyringPair,
+    tx: SubmittableExtrinsic<'promise'>
+  ): Promise<Balance> {
+    const paymentInfo = await tx.paymentInfo(account);
+    return paymentInfo.partialFee;
+  }
+
+  async start(recipient: string, amount: number) {
+    console.log('start transfer', recipient, amount);
+
+    await this.init();
+
+    const selectedAccount: NamedKeyringPair =
+      await this.getFaucetAccount();
+
+    const amountBN: BN = new BN(amount);
+
+    // Initial validation
+    validateAddress(recipient, 'Invalid recipient address');
+
+    const accBalance = await this.getAccountBalancesInfo(selectedAccount.address);
+
+    checkBalance(accBalance, amountBN);
+
+    await this.requestAccountDecoding(selectedAccount);
+
+    console.log(chalk.magentaBright('Estimating fee...'));
+    const tx = await this.createTransferTx(recipient, amountBN);
+    let estimatedFee: BN = new BN(0);
+
+    try {
+      estimatedFee = await this.estimateFee(selectedAccount, tx);
+    } catch (e) {
+      throw new Error('Could not estimate the fee.');
+    }
+
+    const totalAmount: BN = amountBN.add(estimatedFee);
+    console.log(
+      chalk.magentaBright('Estimated fee:', formatBalance(estimatedFee))
+    );
+    console.log(
+      chalk.magentaBright('Total transfer amount:', formatBalance(totalAmount))
+    );
+
+    checkBalance(accBalance, totalAmount);
+
+    try {
+      const txHash: Hash = await tx.signAndSend(selectedAccount);
+      console.log(chalk.greenBright('Transaction successfully sent!'));
+      console.log(chalk.magentaBright('Hash:', txHash.toString()));
+    } catch (e) {
+      console.error('Could not send the transaction.');
+    }
+  }
+
+  async getBalance() {
+    await this.init();
+
+    const selectedAccount: NamedKeyringPair =
+      await this.getFaucetAccount();
+
+    const accBalance = await this.getAccountBalancesInfo(selectedAccount.address);
+
+    return accBalance.availableBalance;
+  }
+
+  async init() {
+    let apiUri: string = DEFAULT_API_URI;
+    if (!apiUri) {
+      console.warn(
+        "You haven't provided a node/endpoint for the bot to connect to yet!"
+      );
+    }
+
+    this.api = await this.initApi(apiUri);
+  }
+
+  private async initApi(
+    apiUri: string = DEFAULT_API_URI,
+  ): Promise<ApiPromise> {
+    const wsProvider: WsProvider = new WsProvider(apiUri);
+    const api = new ApiPromise({
+      provider: wsProvider,
+      types,
+    });
+    await api.isReadyOrError;
+
+    // Initializing some api params based on pioneer/packages/react-api/Api.tsx
+    const [properties] = await Promise.all([api.rpc.system.properties()]);
+
+    const tokenSymbol = properties.tokenSymbol.unwrap()[0].toString();
+    const tokenDecimals = properties.tokenDecimals.unwrap()[0].toNumber();
+
+    // formatBlanace config
+    formatBalance.setDefaults({
+      decimals: tokenDecimals,
+      unit: tokenSymbol,
+    });
+
+    return api;
+  }
+
+  async getFaucetAccount(): Promise<NamedKeyringPair | null> {
+    const jsonBackupFilePath = `../../accounts/${process.env.ACCOUNT_JSON_NAME}`;
+    let accountJsonObj;
+
+    try {
+      accountJsonObj = require(jsonBackupFilePath);
+    } catch (e) {
+      console.error(
+        'Provided backup file is not valid or cannot be accessed'
+      );
+
+      throw new Error('Server error [1]');
+    }
+
+    const keyring = new Keyring();
+    let account: NamedKeyringPair;
+    try {
+      // Try adding and retrieving the keys in order to validate that the backup file is correct
+      keyring.addFromJson(accountJsonObj);
+      account = keyring.getPair(accountJsonObj.address) as NamedKeyringPair; // We can be sure it's named, because we forced it before
+    } catch (e) {
+      console.error('Provided backup file is not valid');
+      throw new Error('Server error [2]');
+    }
+
+    return account;
+  }
+
+  async requestAccountDecoding(account: NamedKeyringPair): Promise<void> {
+    // Skip if account already unlocked
+    if (!account.isLocked) {
+      return;
+    }
+
+    // First - try decoding using empty string
+    try {
+      account.decodePkcs8('');
+      return;
+    } catch (e) {
+      // Continue...
+    }
+
+    let isPassValid = false;
+    while (!isPassValid) {
+      try {
+        const password = process.env.ACCOUNT_PASSWORD;
+        account.decodePkcs8(password);
+        isPassValid = true;
+      } catch (e) {
+        console.warn('Invalid password... Try again.');
+      }
+    }
+  }
+}

+ 25 - 0
community-contributions/joystream-leaderboard-bot/src/commands/validation.ts

@@ -0,0 +1,25 @@
+import BN from 'bn.js';
+import { decodeAddress } from '@polkadot/util-crypto';
+import { DeriveBalancesAll } from '@polkadot/api-derive/types';
+
+export function validateAddress(
+  address: string,
+  errorMessage = 'Invalid address'
+): void {
+  try {
+    decodeAddress(address);
+  } catch (e) {
+    throw new Error(errorMessage);
+  }
+}
+
+export function checkBalance(
+  accBalances: DeriveBalancesAll,
+  requiredBalance: BN
+): void {
+  if (requiredBalance.gt(accBalances.availableBalance)) {
+    throw new Error(
+      'Not enough balance available at the faucet'
+    );
+  }
+}

+ 22 - 1
community-contributions/joystream-leaderboard-bot/src/db.ts

@@ -3,6 +3,7 @@ import mongoose from 'mongoose';
 export interface IMember extends mongoose.Document {
   handle?: String,
   tgId?: Number,
+  enableNotify?: number,
   disId?: Number,
   date?: number,
   lastCommand: String
@@ -11,12 +12,32 @@ export interface IMember extends mongoose.Document {
 const memberSchema = new mongoose.Schema({
   handle: String,
   tgId: Number,
+  enableNotify: Number,
   disId: Number,
-  date: Date,
+  date: Number,
   lastCommand: String
 });
 
+export interface IFaucet extends mongoose.Document {
+  handle?: String,
+  tgId?: Number,
+  disId?: Number,
+  date?: number,
+  dateLastOperation?: number;
+  addresses?: Array<string>[],
+};
+
+const faucetSchema = new mongoose.Schema({
+  handle: String,
+  tgId: Number,
+  disId: Number,
+  date: Number,
+  dateLastOperation: Number,
+  addresses: [String],
+});
+
 export const MemberModel = mongoose.model<IMember>('members', memberSchema);
+export const FaucetModel = mongoose.model<IFaucet>('faucet', faucetSchema);
 
 const dbString = 'mongodb://127.0.0.1:27017/JoystreamLeaderboardBotTest';
 

+ 2 - 3
community-contributions/joystream-leaderboard-bot/src/discord.ts

@@ -1,9 +1,8 @@
 require('dotenv').config();
-import { Client, Intents, Message } from 'discord.js';
+import { Client, Message } from 'discord.js';
 
-import connect, { MemberModel } from './db';
+import connect from './db';
 import botService from './botService';
-// const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES] });
 const client = new Client();
 
 connect();

+ 11 - 2
community-contributions/joystream-leaderboard-bot/src/telegram.ts

@@ -5,10 +5,14 @@ import connect from './db';
 import botService from './botService';
 
 console.log('================ start ================');
+console.log('PRODUCTION', process.env.PRODUCTION);
 
 connect();
 
-const token = process.env.TELEGRAM_BOT_TEST_TOKEN;
+const token =
+  (process.env.PRODUCTION === 'true'
+    ? process.env.TELEGRAM_BOT_TOKEN
+    : process.env.TELEGRAM_BOT_TEST_TOKEN) || '';
 
 const bot = new TelegramBot(token, { polling: true });
 
@@ -23,6 +27,10 @@ bot.setMyCommands([
     command: '/lookup',
     description: 'return the score of the user',
   },
+  {
+    command: '/faucet',
+    description: 'get 101 token',
+  },
 ]);
 
 botService({
@@ -30,7 +38,8 @@ botService({
     bot.sendMessage(message.chat.id, text, { parse_mode: 'Markdown' }),
   commandPrefix: '/',
   client: bot,
-  getId: (message: TelegramBot.Message) => message.from.id,
+  getId: (message: TelegramBot.Message) => message.from?.id,
+  getChatId: (message: TelegramBot.Message) => message.chat.id,
   getText: (message: TelegramBot.Message) => message.text,
   getDate: (message: TelegramBot.Message) => message.date,
   dbId: 'tgId',

+ 14 - 0
community-contributions/joystream-leaderboard-bot/src/types.ts

@@ -0,0 +1,14 @@
+export type SendMessage = (message: any, text: string) => Promise<any>;
+export type CommandPrefix = '/' | '!' | string;
+
+export interface BotServiceProps {
+  send: SendMessage;
+  commandPrefix: CommandPrefix;
+  client: any; // TelegramBot | DiscordBot,
+  getId: Function;
+  getChatId?: Function;
+  getText: Function;
+  getDate: Function;
+  dbId: 'tgId' | 'disId' | string;
+  log: Function;
+}

+ 9 - 8
community-contributions/joystream-leaderboard-bot/tsconfig.json

@@ -1,18 +1,19 @@
 {
   "compilerOptions": {
+    "declaration": true,
+    "importHelpers": true,
     "module": "commonjs",
+    "outDir": "lib",
+    "rootDir": "src",
+    "target": "es2017",
     "esModuleInterop": true,
-    "target": "es6",
-    "noImplicitAny": true,
-    "moduleResolution": "node",
-    "sourceMap": true,
-    "outDir": "dist",
+    "types" : [ "node" ],
     "baseUrl": ".",
     "paths": {
-        "*": [
-            "node_modules/*",
-        ]
+      "@polkadot/types/augment": ["../types/augment-codec/augment-types.ts"]
     },
+    "resolveJsonModule": true,
+    "skipLibCheck": true
   },
   "include": [
     "src/**/*"

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác