Browse Source

bot: 24h heartbeat ; show finalized proposals (#51)

traumschule 4 years ago
parent
commit
d2d26d56c1

+ 4 - 0
community-contributions/joystreamtelegrambot/config.ts

@@ -9,3 +9,7 @@ export const token: string = "";
 
 // telegram chat ID
 export const chatid: string = "";
+
+// time between heartbeat announcement in milliseconds
+export const heartbeat = 60000 * 60 * 24;
+

+ 4 - 7
community-contributions/joystreamtelegrambot/package.json

@@ -23,16 +23,13 @@
     "tests": "ts-node src/tests.ts"
   },
   "dependencies": {
-    "@joystream/types": "^0.13.1",
-    "@polkadot/api": "1.26.1",
-    "@polkadot/keyring": "^3.0.1",
-    "@polkadot/util": "^3.0.1",
-    "@types/node-telegram-bot-api": "^0.50.0",
     "moment": "^2.29.1",
-    "node-telegram-bot-api": "^0.50.0"
+    "node-telegram-bot-api": "^0.51.0",
+    "ws": "^7.4.1"
   },
   "devDependencies": {
-    "@polkadot/ts": "^0.3.49",
+    "@joystream/types": "^0.14.0",
+    "@types/node-telegram-bot-api": "^0.51.0",
     "ts-node": "^9.0.0",
     "ts-node-dev": "^1.0.0-pre.63",
     "typescript": "^4.0.3"

+ 96 - 21
community-contributions/joystreamtelegrambot/src/bot.ts

@@ -1,16 +1,17 @@
 import TelegramBot from "node-telegram-bot-api";
-import { token, chatid, wsLocation } from "../config";
+import { token, chatid, heartbeat, wsLocation } from "../config";
 
 // types
-import { Options, Proposals } from "./types";
+import { Block, Council, Options, Proposals } from "./types";
 import { types } from "@joystream/types";
 import { ApiPromise, WsProvider } from "@polkadot/api";
-import { Header } from "@polkadot/types/interfaces";
+import { AccountId, Header } from "@polkadot/types/interfaces";
 
 // functions
 import * as announce from "./lib/announcements";
 import * as get from "./lib/getters";
-import { parseArgs, printStatus, exit } from "./lib/util";
+import { parseArgs, printStatus, passedTime, exit } from "./lib/util";
+import moment from "moment";
 
 const opts: Options = parseArgs(process.argv.slice(2));
 const log = (msg: string): void | number => opts.verbose && console.log(msg);
@@ -19,8 +20,10 @@ process.env.NTBA_FIX_319 ||
 
 const bot = new TelegramBot(token, { polling: true });
 
+let startTime: number = moment().valueOf();
+
 const sendMessage = (msg: string) => {
-  if (msg === "") return
+  if (msg === "") return;
   try {
     bot.sendMessage(chatid, msg, { parse_mode: "HTML" });
   } catch (e) {
@@ -33,20 +36,36 @@ const main = async () => {
   const api = await ApiPromise.create({ provider, types });
   await api.isReady;
 
-  log(`Publishing to ${chatid} with token ${token}`);
-
   const [chain, node, version] = await Promise.all([
     api.rpc.system.chain(),
     api.rpc.system.name(),
-    api.rpc.system.version()
+    api.rpc.system.version(),
   ]);
 
-  let lastBlock = 0;
+  let council: Council = { round: 0, last: "" };
+  let blocks: Block[] = [];
+  let lastEra = 0;
+  let lastBlock: Block = {
+    id: 0,
+    duration: 6000,
+    timestamp: startTime,
+    stake: 0,
+    noms: 0,
+    vals: 0,
+    issued: 0,
+    reward: 0,
+  };
+  let issued = 0;
+  let reward = 0;
+  let stake = 0;
+  let vals = 0;
+  let noms = 0;
+
   const cats: number[] = [0, 0];
   const channels: number[] = [0, 0];
   const posts: number[] = [0, 0];
   const threads: number[] = [0, 0];
-  let proposals: Proposals = { last: 0, current: 0, active: [], pending: [] };
+  let proposals: Proposals = { last: 0, current: 0, active: [], executing: [] };
 
   if (opts.channel) channels[0] = await get.currentChannelId(api);
 
@@ -58,16 +77,67 @@ const main = async () => {
 
   if (opts.proposals) {
     proposals.last = await get.proposalCount(api);
-    proposals.active = await get.activeProposals(api);
-    proposals.pending = await get.pendingProposals(api);
+    proposals.active = await get.activeProposals(api, proposals.last);
   }
 
+  const getReward = async (era: number) =>
+    Number(await api.query.staking.erasValidatorReward(era));
+
   log(`Subscribed to ${chain} on ${node} v${version}`);
-  const unsubscribe = await api.rpc.chain.subscribeNewHeads(
-    async (block: Header): Promise<void> => {
-      const currentBlock = block.number.toNumber();
-      if (opts.council && currentBlock > lastBlock)
-        lastBlock = await announce.councils(api, currentBlock, sendMessage);
+  api.rpc.chain.subscribeNewHeads(
+    async (header: Header): Promise<void> => {
+      // current block
+      const id = header.number.toNumber();
+      if (lastBlock.id === id) return;
+      const timestamp = (await api.query.timestamp.now()).toNumber();
+      const duration = timestamp - lastBlock.timestamp;
+
+      // update validators and nominators every era
+      const era = Number(await api.query.staking.currentEra());
+
+      if (era > lastEra) {
+        vals = Number(await api.query.staking.validatorCount());
+        stake = Number(await api.query.staking.erasTotalStake(era));
+        issued = Number(await api.query.balances.totalIssuance());
+        reward = (await getReward(era - 1)) || (await getReward(era - 2));
+
+        // nominator count
+        noms = 0;
+        const nominators: { [key: string]: number } = {};
+        const stashes = (await api.derive.staking.stashes())
+          .map((s) => String(s))
+          .map(async (v) => {
+            const stakers = await api.query.staking.erasStakers(era, v);
+            stakers.others.forEach(
+              (n: { who: AccountId }) => nominators[String(n.who)]++
+            );
+            noms = Object.keys(nominators).length;
+          });
+        lastEra = era;
+      }
+
+      const block: Block = {
+        id,
+        timestamp,
+        duration,
+        stake,
+        noms,
+        vals,
+        reward,
+        issued,
+      };
+      blocks = blocks.concat(block);
+
+      // heartbeat
+      if (timestamp > startTime + heartbeat) {
+        const time = passedTime(startTime, timestamp);
+        blocks = announce.heartbeat(api, blocks, time, proposals, sendMessage);
+        startTime = block.timestamp;
+      }
+
+      // announcements
+      if (opts.council && block.id > lastBlock.id)
+        council = await announce.council(api, council, block.id, sendMessage);
 
       if (opts.channel) {
         channels[1] = await get.currentChannelId(api);
@@ -77,8 +147,13 @@ const main = async () => {
 
       if (opts.proposals) {
         proposals.current = await get.proposalCount(api);
-        if (proposals.current > proposals.last)
-          proposals = await announce.proposals(api, proposals, sendMessage);
+        if (
+          proposals.current > proposals.last ||
+          proposals.active ||
+          proposals.executing
+        )
+          // TODO do not refetch each active/executing proposal on every block
+          proposals = await announce.proposals(api, proposals, id, sendMessage);
       }
 
       if (opts.forum) {
@@ -97,12 +172,12 @@ const main = async () => {
       }
 
       printStatus(opts, {
-        block: currentBlock,
+        block: id,
         cats,
         chain: String(chain),
         posts,
         proposals,
-        threads
+        threads,
       });
     }
   );

+ 150 - 77
community-contributions/joystreamtelegrambot/src/lib/announcements.ts

@@ -1,17 +1,27 @@
-import { Api, Member, ProposalDetail, Proposals } from "../types";
+import {
+  Api,
+  Block,
+  Council,
+  Member,
+  ProposalDetail,
+  Proposals,
+} from "../types";
 import { BlockNumber } from "@polkadot/types/interfaces";
 import { Channel, ElectionStage } from "@joystream/types/augment";
 import { Category, Thread, Post } from "@joystream/types/forum";
 import { domain } from "../../config";
+import { formatTime } from "./util";
 import {
   categoryById,
   memberHandle,
   memberHandleByAccount,
-  proposalDetail
+  proposalDetail,
 } from "./getters";
+import moment from "moment";
 
-const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
 
+// query API repeatedly to ensure a result
 const query = async (test: string, cb: () => Promise<any>): Promise<any> => {
   let result = await cb();
   for (let i: number = 0; i < 10; i++) {
@@ -21,6 +31,7 @@ const query = async (test: string, cb: () => Promise<any>): Promise<any> => {
   }
 };
 
+// announce latest channels
 export const channels = async (
   api: Api,
   channels: number[],
@@ -29,7 +40,7 @@ export const channels = async (
   const [last, current] = channels;
   const messages: string[] = [];
 
-  for (let id: number = last + 1; id <= current; id++) {
+  for (let id: number = +last + 1; id <= current; id++) {
     const channel: Channel = await query("title", () =>
       api.query.contentWorkingGroup.channelById(id)
     );
@@ -44,61 +55,78 @@ export const channels = async (
   return current;
 };
 
-export const councils = async (
+// announce council change
+
+export const council = async (
   api: Api,
+  council: Council,
   currentBlock: number,
   sendMessage: (msg: string) => void
-): Promise<number> => {
-  let lastBlock: number = currentBlock;
+): Promise<Council> => {
   const round: number = await api.query.councilElection.round();
-  const stage: ElectionStage | null = await api.query.councilElection.stage();
+  const stage: any = await api.query.councilElection.stage();
+  const stageObj = JSON.parse(JSON.stringify(stage));
+  let stageString = stageObj ? Object.keys(stageObj)[0] : "";
   let msg = "";
-  if (!stage) {
+
+  if (!stage || stage.toJSON() === null) {
+    stageString = "elected";
     const councilEnd: BlockNumber = await api.query.council.termEndsAt();
-    lastBlock = councilEnd.toNumber();
     const termDuration: BlockNumber = await api.query.councilElection.newTermDuration();
-    const block = lastBlock - termDuration.toNumber();
-    msg = `<a href="${domain}/#/council/members">Council for round ${round}</a> has been elected at block ${block} until block ${councilEnd}.`;
+    const block = councilEnd.toNumber() - termDuration.toNumber();
+    if (currentBlock - block < 2000) {
+      const remainingBlocks: number = councilEnd.toNumber() - currentBlock;
+      const endDate = moment()
+        .add(remainingBlocks * 6, "s")
+        .format("DD/MM/YYYY");
+
+      const handles: string[] = await Promise.all(
+        (await api.query.council.activeCouncil()).map(
+          async (seat: { member: string }) =>
+            await memberHandleByAccount(api, seat.member)
+        )
+      );
+      const members = handles.join(", ");
+
+      msg = `${members} have been elected for <a href="${domain}/#/council/members">council ${round}</a>. Congratulations!\nNext election starts on ${endDate}.`;
+    }
   } else {
-    if (stage.isAnnouncing) {
-      lastBlock = stage.asAnnouncing.toNumber();
+    if (stageString === "Announcing") {
       const announcingPeriod: BlockNumber = await api.query.councilElection.announcingPeriod();
-      const block = lastBlock - announcingPeriod.toNumber();
-      msg = `Announcing election for round ${round} at ${block}.<a href="${domain}/#/council/applicants">Apply now!</a>`;
-    } else if (stage.isVoting) {
-      lastBlock = stage.asVoting.toNumber();
+      msg = `Announcing election for round ${round} started.<a href="${domain}/#/council/applicants">Apply now!</a>`;
+    } else if (stageString === "Voting") {
       const votingPeriod: BlockNumber = await api.query.councilElection.votingPeriod();
-      const block = lastBlock - votingPeriod.toNumber();
-      msg = `Voting stage for council election started at block ${block}. <a href="${domain}/#/council/applicants">Vote now!</a>`;
-    } else if (stage.isRevealing) {
-      lastBlock = stage.asRevealing.toNumber();
+      msg = `Voting stage for council election started. <a href="${domain}/#/council/applicants">Vote now!</a>`;
+    } else if (stageString === "Revealing") {
       const revealingPeriod: BlockNumber = await api.query.councilElection.revealingPeriod();
-      const block = lastBlock - revealingPeriod.toNumber();
-      msg = `Revealing stage for council election started at block ${block}. <a href="${domain}/#/council/votes">Don't forget to reveal your vote!</a>`;
-    }
+      msg = `Revealing stage for council election started. <a href="${domain}/#/council/votes">Don't forget to reveal your vote!</a>`;
+    } else console.log(`[council] unrecognized stage: ${stageString}`);
   }
-  sendMessage(msg);
-  return lastBlock;
+
+  if (round !== council.round && stageString !== council.last) sendMessage(msg);
+  return { round, last: stageString };
 };
 
 // forum
-
+// announce latest categories
 export const categories = async (
   api: Api,
   category: number[],
   sendMessage: (msg: string) => void
 ): Promise<number> => {
   const messages: string[] = [];
-  let id: number = category[0] + 1;
-  for (id; id <= category[1]; id++) {
+
+  for (let id: number = +category[0] + 1; id <= category[1]; id++) {
     const cat: Category = await query("title", () => categoryById(api, id));
     const msg = `Category ${id}: <b><a href="${domain}/#/forum/categories/${id}">${cat.title}</a></b>`;
     messages.push(msg);
   }
+
   sendMessage(messages.join("\r\n\r\n"));
   return category[1];
 };
 
+// announce latest posts
 export const posts = async (
   api: Api,
   posts: number[],
@@ -106,8 +134,8 @@ export const posts = async (
 ): Promise<number> => {
   const [last, current] = posts;
   const messages: string[] = [];
-  let id: number = last + 1;
-  for (id; id <= current; id++) {
+
+  for (let id: number = +last + 1; id <= current; id++) {
     const post: Post = await query("current_text", () =>
       api.query.forum.postById(id)
     );
@@ -126,10 +154,12 @@ export const posts = async (
     const msg = `<b><a href="${domain}/#/members/${handle}">${handle}</a> posted <a href="${domain}/#/forum/threads/${threadId}?replyIdx=${replyId}">${threadTitle}</a> in <a href="${domain}/#/forum/categories/${category.id}">${category.title}</a>:</b>\n\r<i>${excerpt}</i> <a href="${domain}/#/forum/threads/${threadId}?replyIdx=${replyId}">more</a>`;
     messages.push(msg);
   }
+
   sendMessage(messages.join("\r\n\r\n"));
   return current;
 };
 
+// announce latest threads
 export const threads = async (
   api: Api,
   threads: number[],
@@ -137,8 +167,8 @@ export const threads = async (
 ): Promise<number> => {
   const [last, current] = threads;
   const messages: string[] = [];
-  let id: number = last + 1;
-  for (id; id <= current; id++) {
+
+  for (let id: number = +last + 1; id <= current; id++) {
     const thread: Thread = await query("title", () =>
       api.query.forum.threadById(id)
     );
@@ -150,65 +180,108 @@ export const threads = async (
     const msg = `Thread ${id}: <a href="${domain}/#/forum/threads/${id}">"${title}"</a> by <a href="${domain}/#/members/${handle}">${handle}</a> in category "<a href="${domain}/#/forum/categories/${category.id}">${category.title}</a>" `;
     messages.push(msg);
   }
+
   sendMessage(messages.join("\r\n\r\n"));
-  return id;
+  return current;
 };
 
-// proposals
-
-const processActive = async (
-  id: number,
-  details: ProposalDetail,
-  sendMessage: (s: string) => void
-): Promise<boolean> => {
-  const { createdAt, finalizedAt, message, parameters, result } = details;
-  let msg = `Proposal ${id} <b>created</b> at block ${createdAt}.\r\n${message}`;
-  if (details.stage === "Finalized") {
-    let label: string = result;
-    if (result === "Approved") {
-      const executed = parameters.gracePeriod.toNumber() > 0 ? false : true;
-      label = executed ? "Finalized" : "Executed";
+// announce latest proposals
+export const proposals = async (
+  api: Api,
+  prop: Proposals,
+  block: number,
+  sendMessage: (msg: string) => void
+): Promise<Proposals> => {
+  let { current, last, active, executing } = prop;
+
+  for (let id: number = +last + 1; id <= current; id++) {
+    const proposal: ProposalDetail = await proposalDetail(api, id);
+    const { createdAt, finalizedAt, message, parameters, result } = proposal;
+    const votingEndsAt = createdAt + parameters.votingPeriod.toNumber();
+    const msg = `Proposal ${id} <b>created</b> at block ${createdAt}.\r\n${message}\r\nYou can vote until block ${votingEndsAt}.`;
+    sendMessage(msg);
+    active.push(id);
+  }
+
+  for (const id of active) {
+    const proposal: ProposalDetail = await proposalDetail(api, id);
+    const { finalizedAt, message, parameters, result, stage } = proposal;
+    if (stage === "Finalized") {
+      let label: string = result;
+      if (result === "Approved") {
+        const executed = parameters.gracePeriod.toNumber() > 0 ? false : true;
+        label = executed ? "Executed" : "Finalized";
+        if (!executed) executing.push(id);
+      }
+      const msg = `Proposal ${id} <b>${label}</b> at block ${finalizedAt}.\r\n${message}`;
+      sendMessage(msg);
+      active = active.filter((a) => a !== id);
     }
-    msg = `Proposal ${id} <b>${label}</b> at block ${finalizedAt}.\r\n${message}`;
+  }
+
+  for (const id of executing) {
+    const proposal = await proposalDetail(api, id);
+    const { exec, finalizedAt, message, parameters } = proposal;
+    const executesAt = +finalizedAt + parameters.gracePeriod.toNumber();
+    if (block < executesAt) continue;
+    const execStatus = exec ? Object.keys(exec)[0] : "";
+    const label = execStatus === "Executed" ? "has been" : "failed to be";
+    const msg = `Proposal ${id} <b>${label} executed</b> at block ${executesAt}.\r\n${message}`;
     sendMessage(msg);
-    return true;
-  } else return processPending(id, details, sendMessage);
-};
+    executing = executing.filter((e) => e !== id);
+  }
 
-const processPending = async (
-  id: number,
-  details: ProposalDetail,
-  sendMessage: (s: string) => void
-): Promise<boolean> => {
-  const { createdAt, message, parameters, stage } = details;
-  if (stage === "Finalized") return processActive(id, details, sendMessage);
-  const votingEndsAt = createdAt + parameters.votingPeriod.toNumber();
-  const msg = `Proposal ${id} <b>created</b> at block ${createdAt}.\r\n${message}\r\nYou can vote until block ${votingEndsAt}.`;
-  sendMessage(msg);
-  return true;
+  return { current, last: current, active, executing };
 };
 
-export const proposals = async (
+// heartbeat
+
+const getAverage = (array: number[]): number =>
+  array.reduce((a: number, b: number) => a + b, 0) / array.length;
+
+export const heartbeat = (
   api: Api,
-  prop: Proposals,
+  blocks: Block[],
+  timePassed: string,
+  proposals: Proposals,
   sendMessage: (msg: string) => void
-): Promise<Proposals> => {
-  let { current, last, active, pending } = prop;
+): [] => {
+  const durations = blocks.map((b) => b.duration);
+  const blocktime = getAverage(durations) / 1000;
+
+  const stake = blocks.map((b) => b.stake);
+  const avgStake = getAverage(stake) / 1000000;
+  const issued = blocks.map((b) => b.issued);
+  const avgIssued = getAverage(issued) / 1000000;
+  const percent = ((100 * avgStake) / avgIssued).toFixed(2);
 
-  for (let id: number = last++; id <= current; id++) active.push(id);
+  const noms = blocks.map((b) => b.noms);
+  const vals = blocks.map((b) => b.vals);
+  const avgVals = getAverage(vals);
+  const totalReward = blocks.map((b) => b.reward);
+  const avgReward = getAverage(totalReward);
+  const reward = (avgReward / avgVals).toFixed();
 
-  for (const id of active)
-    if (processActive(id, await proposalDetail(api, id), sendMessage))
-      active = active.filter((e: number) => e !== id);
+  const active = proposals.active.length;
+  const executing = proposals.executing.length;
+  const p = (n: number) => (n > 1 ? "proposals" : "proposal");
+  let props = active
+    ? `\n<a href="${domain}/#/proposals">${active} active ${p(active)}</a> `
+    : "";
+  if (executing) props += `{executing} ${p(executing)} to be executed.`;
 
-  for (const id of pending)
-    if (processPending(id, await proposalDetail(api, id), sendMessage))
-      pending = pending.filter((e: number) => e !== id);
+  sendMessage(
+    `  ${blocks.length} blocks produced in ${timePassed}h
+  Blocktime: ${blocktime.toFixed(3)}s
+  Stake: ${avgStake.toFixed(1)} / ${avgIssued.toFixed()} M tJOY (${percent}%)
+  Validators: ${avgVals.toFixed()} (${reward} tJOY/h)
+  Nominators: ${getAverage(noms).toFixed()}` + props
+  );
 
-  return { current, last: current, active, pending };
+  return [];
 };
 
 export const formatProposalMessage = (data: string[]): string => {
   const [id, title, type, stage, result, handle] = data;
-  return `<b>Type</b>: ${type}\r\n<b>Proposer</b>:<a href="${domain}/#/members/${handle}"> ${handle}</a>\r\n<b>Title</b>: <a href="${domain}/#/proposals/${id}">${title}</a>\r\n<b>Stage</b>: ${stage}\r\n<b>Result</b>: ${result}`;
+  return `<b>Type</b>: ${type}\r\n<b>Proposer</b>: <a href="${domain}/#/members/${handle}">${handle}</a>\r\n<b>Title</b>: <a href="${domain}/#/proposals/${id}">${title}</a>\r\n<b>Stage</b>: ${stage}\r\n<b>Result</b>: ${result}`;
 };

+ 20 - 29
community-contributions/joystreamtelegrambot/src/lib/getters.ts

@@ -7,10 +7,10 @@ import {
   ChannelId,
   PostId,
   ProposalDetailsOf,
-  ThreadId
+  ThreadId,
 } from "@joystream/types/augment";
 import { Category, CategoryId } from "@joystream/types/forum";
-import { MemberId,Membership } from "@joystream/types/members";
+import { MemberId, Membership } from "@joystream/types/members";
 import { Proposal } from "@joystream/types/proposals";
 
 // channel
@@ -29,7 +29,9 @@ export const memberHandleByAccount = async (
   api: Api,
   account: string
 ): Promise<string> => {
-  const id: MemberId = await api.query.members.memberIdsByRootAccountId(account);
+  const id: MemberId = await api.query.members.memberIdsByRootAccountId(
+    account
+  );
   const handle: string = await memberHandle(api, id);
   return handle;
 };
@@ -58,32 +60,20 @@ export const currentCategoryId = async (api: Api): Promise<number> => {
 
 // proposals
 
-export const proposalCount = async (api: Api): Promise<number> => {
-  const proposalCount: number = await api.query.proposalsEngine.proposalCount();
-  return proposalCount || 0;
-};
+export const proposalCount = async (api: Api): Promise<number> =>
+  Number(await api.query.proposalsEngine.proposalCount());
 
-const activeProposalCount = async (api: Api): Promise<number> => {
-  const proposalCount: number = await api.query.proposalsEngine.activeProposalCount();
-  return proposalCount || 0;
-};
-
-export const pendingProposals = async (api: Api): Promise<ProposalArray> => {
-  const pending: ProposalArray = await api.query.proposalsEngine.pendingExecutionProposalIds(
-    await activeProposalCount(api)
-  );
-  //const pending: ProposalArray = pendingProposals.toJSON();
-  if (pending.length) console.debug("pending proposals", pending);
-  return pending;
-};
-
-export const activeProposals = async (api: Api): Promise<ProposalArray> => {
-  const active: ProposalArray = await api.query.proposalsEngine.activeProposalIds(
-    await activeProposalCount(api)
-  );
-  //const active: ProposalArray = result.toJSON();
-  if (active.length) console.debug("active proposals", active);
-  return active;
+export const activeProposals = async (
+  api: Api,
+  last: number
+): Promise<number[]> => {
+  const count = Number(await api.query.proposalsEngine.activeProposalCount());
+  let ids: number[] = [];
+  for (let id = last; ids.length < count; id--) {
+    const proposal = await proposalDetail(api, id);
+    if (proposal.result === "Pending") ids.push(id);
+  }
+  return ids;
 };
 
 const getProposalType = async (api: Api, id: number): Promise<string> => {
@@ -110,6 +100,7 @@ export const proposalDetail = async (
       (proposalStatus.isSlashed && "Slashed") ||
       (proposalStatus.isVetoed && "Vetoed")
     : "Pending";
+  const exec = proposalStatus ? proposalStatus["Approved"] : null;
 
   const { parameters, proposerId } = proposal;
   const author: string = await memberHandle(api, proposerId);
@@ -118,5 +109,5 @@ export const proposalDetail = async (
   const args: string[] = [String(id), title, type, stage, result, author];
   const message: string = formatProposalMessage(args);
   const createdAt: number = proposal.createdAt.toNumber();
-  return { createdAt, finalizedAt, parameters, message, stage, result };
+  return { createdAt, finalizedAt, parameters, message, stage, result, exec };
 };

+ 10 - 4
community-contributions/joystreamtelegrambot/src/lib/util.ts

@@ -1,5 +1,4 @@
-import { Options } from "../types";
-import { Proposals } from "../types";
+import { Api, Options, Proposals } from "../types";
 import moment from "moment";
 
 export const parseArgs = (args: string[]): Options => {
@@ -33,18 +32,25 @@ export const printStatus = (
   if (opts.verbose < 1) return;
 
   const { block, chain, proposals, cats, posts, threads } = data;
-  const date = moment().format("L HH:mm:ss");
+  const date = formatTime();
   let message = `[${date}] Chain:${chain} Block:${block} `;
 
   if (opts.forum)
     message += `Post:${posts[1]} Cat:${cats[1]} Thread:${threads[1]} `;
 
   if (opts.proposals)
-    message += `Proposals:${proposals.current} (Active:${proposals.active.length} Pending:${proposals.pending.length}) `;
+    message += `Proposals:${proposals.current} (Active:${proposals.active.length} Pending:${proposals.executing.length}) `;
 
   console.log(message);
 };
 
+// time
+export const formatTime = (time?: any): string =>
+  moment(time).format("H:mm:ss");
+
+export const passedTime = (start: number, now: number): string =>
+  formatTime(moment.utc(moment(now).diff(moment(start))));
+
 export const exit = (log: (s: string) => void) => {
   log("\nNo connection, exiting.\n");
   process.exit();

+ 1 - 1
community-contributions/joystreamtelegrambot/src/tests.ts

@@ -32,7 +32,7 @@ const main = async () => {
     last: 1,
     current: 2,
     active: [],
-    pending: []
+    executing: []
   };
   let categories = [0, 0];
   let posts = [0, 0];

+ 21 - 1
community-contributions/joystreamtelegrambot/src/types/index.ts

@@ -2,11 +2,19 @@ import { ApiPromise } from "@polkadot/api";
 import { MemberId } from "@joystream/types/members";
 import { AnyJson } from "@polkadot/types/types/helpers";
 import { ProposalParameters, ProposalStatus } from "@joystream/types/proposals";
+import { Nominations } from "@polkadot/types/interfaces";
+import { Option } from "@polkadot/types/codec";
+import { StorageKey } from "@polkadot/types/primitive";
 
 export interface Api {
   query: any;
 }
 
+export interface Council {
+  round: number;
+  last: string;
+}
+
 export interface Options {
   verbose: number;
   channel: boolean;
@@ -22,6 +30,7 @@ export interface ProposalDetail {
   parameters: ProposalParameters;
   stage: string;
   result: string;
+  exec: any;
 }
 
 export type ProposalArray = number[];
@@ -30,7 +39,7 @@ export interface Proposals {
   current: number;
   last: number;
   active: ProposalArray;
-  pending: ProposalArray;
+  executing: ProposalArray;
 }
 
 export interface Member {
@@ -38,3 +47,14 @@ export interface Member {
   handle: string;
   url?: string;
 }
+
+export interface Block {
+  id: number;
+  timestamp: number;
+  duration: number;
+  stake: number;
+  noms: number;
+  vals: number;
+  issued: number;
+  reward: number;
+}