Browse Source

use joystream-lib + proposal reminders

Joystream Stats 3 years ago
parent
commit
62e1ac2634

+ 3 - 0
.gitmodules

@@ -1,3 +1,6 @@
 [submodule "lib"]
 	path = scripts/report-generator/src/lib
 	url = https://git.joystreamstats.live/Operations/joystream-lib
+[submodule "scripts/joystreamtelegrambot/src/lib"]
+	path = scripts/joystreamtelegrambot/src/lib
+	url = https://git.joystreamstats.live/Operations/joystream-lib

+ 10 - 5
scripts/joystreamtelegrambot/config.ts

@@ -1,10 +1,16 @@
 // website
 export const domain = "https://pioneer.joystreamstats.live";
+export const apiUrl = "https://api.joystreamstats.live/api";
 
 // websocket location
-export const wsLocation = "wss://joystreamstats.live:9945";
-
-export const discordToken = ""
+export const wsLocation = "wss://rome-rpc-endpoint.joystream.org:9944"; //"wss://joystreamstats.live:9945";
+export const statusEndpoint = [
+  "https://joystreamstats.live/static/status.json",
+  "https://status.joystream.org/status",
+];
+
+// bot tokens
+export const discordToken = "";
 export const tgToken = "";
 
 // telegram chat ID
@@ -13,10 +19,9 @@ export const chatid = "-1001438587296";
 // time between heartbeat announcement in milliseconds
 export const heartbeat = 60000 * 60 * 6;
 
-export const councilStatusHeartbeat = 300000; // 86400000;
+export const councilStatusHeartbeat = 86400000;
 
 // minutes between checking for proposal updates
 export const proposalDelay = 15;
 
 export const suppressedThreads = [180, 265, 275, 390]; // 180 tokens, 265 faucet, 275 pets, 390 bounty-24
-

+ 108 - 105
scripts/joystreamtelegrambot/src/lib/announcements.ts → scripts/joystreamtelegrambot/src/announcements.ts

@@ -1,29 +1,31 @@
-import { suppressedThreads } from "../../config";
 import {
-  Api,
-  Block,
-  Council,
-  Member,
-  ProposalDetail,
-  Proposals,
-  Send,
-} from "../types";
-import { BlockNumber } from "@polkadot/types/interfaces";
+  getBlockHash,
+  getCouncil,
+  getCouncilRound,
+  getCouncilElectionStatus,
+  getPost,
+  getThread,
+  getCategory,
+  getMemberHandle,
+  getMemberHandleByAccount,
+  getProposal,
+} from "./lib/api";
+import { fetchTokenValue, formatTime } from "./util";
+import moment, { now } from "moment";
+
+// types
+import { Block, Council, Member, MemberHandles, Send } from "./types";
+import { Proposals, ProposalVotes } from "./types";
+import { ProposalDetail } from "./lib/types";
+import { ApiPromise } from "@polkadot/api";
+import { AccountId, BlockNumber } from "@polkadot/types/interfaces";
 import { Channel, ElectionStage } from "@joystream/types/augment";
+import { Seats } from "@joystream/types/council";
 import { Category, Thread, Post } from "@joystream/types/forum";
 import { DiscussionPost } from "@joystream/types/proposals";
-import { domain } from "../../config";
-import { formatTime } from "./util";
-import {
-  categoryById,
-  memberHandle,
-  memberHandleByAccount,
-  proposalDetail,
-  fetchTokenValue,
-  fetchStorageSize,
-} from "./getters";
-import moment, { now } from "moment";
 
+// config
+import { domain, suppressedThreads } from "../config";
 const dateFormat = "DD-MM-YYYY HH:mm (UTC)";
 const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
 
@@ -39,7 +41,7 @@ const query = async (test: string, cb: () => Promise<any>): Promise<any> => {
 
 // announce latest channels
 export const channels = async (
-  api: Api,
+  api: ApiPromise,
   channels: number[],
   sendMessage: Send,
   channel: any
@@ -52,7 +54,7 @@ export const channels = async (
       api.query.contentWorkingGroup.channelById(id)
     );
     const member: Member = { id: channel.owner.asMember, handle: "", url: "" };
-    member.handle = await memberHandle(api, member.id);
+    member.handle = await getMemberHandle(api, member.id);
     member.url = `${domain}/#/members/${member.handle}`;
     messages[0].push(
       `<b>Channel <a href="${domain}/#//media/channels/${id}">${id}</a> by <a href="${member.url}">${member.handle} (${member.id})</a></b>`
@@ -74,41 +76,31 @@ export const channels = async (
 // announce council change
 
 export const council = async (
-  api: Api,
+  api: ApiPromise,
   council: Council,
   currentBlock: number,
   sendMessage: Send,
   channel: any
 ): Promise<Council> => {
-  const round: number = await api.query.councilElection.round();
-  const stage: any = await api.query.councilElection.stage();
+  const hash = await getBlockHash(api, currentBlock);
+  const election = await getCouncilElectionStatus(api, hash);
+  const { round, stage, termEndsAt, stageEndsAt, durations } = election;
   const stageObj = JSON.parse(JSON.stringify(stage));
   let stageString = stageObj ? Object.keys(stageObj)[0] : "";
   let msg: string[] = ["", ""];
 
   if (!stage || stage.toJSON() === null) {
     stageString = "elected";
-    const councilEnd: BlockNumber = await api.query.council.termEndsAt();
-    const termDuration: BlockNumber =
-      await api.query.councilElection.newTermDuration();
-    const block = councilEnd.toNumber() - termDuration.toNumber();
-    if (currentBlock - block < 2000) {
-      const remainingBlocks = councilEnd.toNumber() - currentBlock;
-      const m = moment().add(remainingBlocks * 6, "s");
-      const endDate = formatTime(m, dateFormat);
-      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[0] = `Council election ended: ${members} have been elected for <a href="${domain}/#/council/members">council ${round}</a>. Congratulations!\nNext election starts on ${endDate}.`;
-      msg[1] = `Council election ended: ${members} have been elected for council ${round}. Congratulations!\nNext election starts on ${endDate}.\n${domain}/#/council/members`;
-    }
+    const remainingBlocks = termEndsAt - currentBlock;
+    const m = moment().add(remainingBlocks * 6, "s");
+    const endDate = formatTime(m, dateFormat);
+    const councilEnd = await api.query.council.termEndsAt();
+    const termDuration = await api.query.councilElection.newTermDuration();
+    const membersDc = council.seats.map((s) => `<${s.discord}>`).join(" ");
+    const membersTg = council.seats.map((s) => s.telegram).join(" ");
+
+    msg[0] = `Council election ended: ${membersDc} have been elected for <a href="${domain}/#/council/members">council ${round}</a>. Congratulations!\nNext election starts on ${endDate}.`;
+    msg[1] = `Council election ended: ${membersTg} have been elected for council ${round}. Congratulations!\nNext election starts on ${endDate}.\n${domain}/#/council/members`;
   } else {
     const remainingBlocks = stage.toJSON()[stageString] - currentBlock;
     const m = moment().add(remainingBlocks * 6, "second");
@@ -126,58 +118,38 @@ export const council = async (
       msg[1] = `Council election: **Reveal your votes** until ${endDate} ${link}votes`;
     }
   }
-
-  if (
-    council.last !== "" &&
-    round !== council.round &&
-    stageString !== council.last
-  ) {
+  const notFirst = council.last.length;
+  const isChanged = round !== council.round && stageString !== council.last;
+  if (notFirst && isChanged)
     sendMessage({ tg: msg[0], discord: msg[1], tgParseMode: "HTML" }, channel);
-  }
-  return { round, last: stageString };
+
+  return { ...council, round, last: stageString };
 };
 
 export const councilStatus = async (
-  api: Api,
+  api: ApiPromise,
   block: Block,
   sendMessage: Send,
   channel: any
 ): Promise<void> => {
   const currentBlock = block.id;
-  const councilTermEndBlock: number = (
-    await api.query.council.termEndsAt()
-  ).toJSON();
-  const announcingPeriod: number = (
-    await api.query.councilElection.announcingPeriod()
-  ).toJSON();
-  const votingPeriod: number = (
-    await api.query.councilElection.votingPeriod()
-  ).toJSON();
-  const revealingPeriod: number = (
-    await api.query.councilElection.revealingPeriod()
-  ).toJSON();
-  const stage: any = await api.query.councilElection.stage();
+  const hash = await getBlockHash(api, currentBlock);
+  const election = await getCouncilElectionStatus(api, hash);
+  const { round, stage, termEndsAt, stageEndsAt, durations } = election;
   const stageObj = JSON.parse(JSON.stringify(stage));
   let stageString = stageObj ? Object.keys(stageObj)[0] : "";
 
   let stageEndDate = moment();
   if (!stage || stage.toJSON() === null) {
     stageString = "elected";
-    const councilEnd: BlockNumber = await api.query.council.termEndsAt();
-    const termDuration: BlockNumber =
-      await api.query.councilElection.newTermDuration();
-    const block = councilEnd.toNumber() - termDuration.toNumber();
-    if (currentBlock - block < 2000) {
-      const remainingBlocks = councilEnd.toNumber() - currentBlock;
-      stageEndDate = moment().add(remainingBlocks * 6, "s");
-    }
+    const remainingBlocks = termEndsAt - currentBlock;
+    stageEndDate = moment().add(remainingBlocks * 6, "s");
   } else {
     const remainingBlocks = stage.toJSON()[stageString] - currentBlock;
     stageEndDate = moment().add(remainingBlocks * 6, "second");
   }
 
-  const revealingEndsAt =
-    councilTermEndBlock + announcingPeriod + votingPeriod + revealingPeriod;
+  const revealingEndsAt = termEndsAt + durations[4] - durations[3];
   const termBlocksRemaining = revealingEndsAt - currentBlock;
   let councilEndDate = moment().add(termBlocksRemaining * 6, "seconds");
   let councilEndDateString = formatTime(councilEndDate, dateFormat);
@@ -212,7 +184,7 @@ export const councilStatus = async (
 // forum
 // announce latest categories
 export const categories = async (
-  api: Api,
+  api: ApiPromise,
   category: number[],
   sendMessage: Send,
   channel: any
@@ -221,7 +193,7 @@ export const categories = async (
   const messages: string[][] = [[], []];
 
   for (let id: number = +category[0] + 1; id <= category[1]; id++) {
-    const cat: Category = await query("title", () => categoryById(api, id));
+    const cat: Category = await query("title", () => getCategory(api, id));
     messages[0].push(
       `Category ${id}: <b><a href="${domain}/#/forum/categories/${id}">${cat.title}</a></b>`
     );
@@ -243,7 +215,7 @@ export const categories = async (
 
 // announce latest posts
 export const posts = async (
-  api: Api,
+  api: ApiPromise,
   posts: number[],
   sendMessage: Send,
   channel: any
@@ -253,22 +225,16 @@ export const posts = async (
   const messages: string[][] = [[], []];
 
   for (let id: number = +last + 1; id <= current; id++) {
-    const post: Post = await query("current_text", () =>
-      api.query.forum.postById(id)
-    );
+    const post: Post = await query("current_text", () => getPost(api, id));
     const replyId: number = post.nr_in_thread.toNumber();
     const threadId: number = post.thread_id.toNumber();
-    const thread: Thread = await query("title", () =>
-      api.query.forum.threadById(threadId)
-    );
-    const categoryId = thread.category_id.toNumber();
-    if (categoryId === 19 || categoryId === 38) continue; // hide: 19 Media, 38 Russian
+    const thread: Thread = await query("title", () => getThread(api, threadId));
     if (suppressedThreads.includes(threadId)) continue;
 
     const category: Category = await query("title", () =>
-      categoryById(api, categoryId)
+      getCategory(api, thread.category_id.toNumber())
     );
-    const handle = await memberHandleByAccount(api, post.author_id.toJSON());
+    const handle = await getMemberHandleByAccount(api, post.author_id);
 
     const s = {
       content: post.current_text.substring(0, 250),
@@ -295,37 +261,39 @@ export const posts = async (
 };
 
 export const proposalCreated = (
-  proposal: ProposalDetail | undefined,
+  proposal: ProposalDetail,
   sendMessage: Send,
   channel: any
 ): void => {
   if (!proposal) return;
-  const { id, createdAt, finalizedAt, message, parameters, result } = proposal;
-  if (!createdAt) return console.warn(`proposalCreated: wrong data`, proposal);
-  const votingEndsAt = createdAt + parameters.votingPeriod.toNumber();
+  const { id, created, finalizedAt, message, result } = proposal;
+  if (!created) return console.warn(`proposalCreated: wrong data`, proposal);
+  const { votingPeriod } = JSON.parse(proposal.parameters);
+  const votingEndsAt = created + votingPeriod;
   const endTime = moment()
-    .add(6 * parameters.votingPeriod.toNumber(), "second")
+    .add(6 * votingPeriod, "second")
     .format("DD/MM/YYYY HH:mm");
   const link = `${domain}/#/proposals/${id}`;
-  const tg = `<a href="${link}">Proposal ${id}</a> <b>created</b> at block ${createdAt}.\r\n${message.tg}\r\nYou can <a href="${link}">vote</a> until block ${votingEndsAt} (${endTime} UTC).`;
-  const discord = `Proposal ${id} **created** at block ${createdAt}.\n${message.discord}\nVote until block ${votingEndsAt} (${endTime} UTC): ${link}\n`;
+  const tg = `<a href="${link}">Proposal ${id}</a> <b>created</b> at block ${created}.\r\n${message.tg}\r\nYou can <a href="${link}">vote</a> until block ${votingEndsAt} (${endTime} UTC).`;
+  const discord = `Proposal ${id} **created** at block ${created}.\n${message.discord}\nVote until block ${votingEndsAt} (${endTime} UTC): ${link}\n`;
   sendMessage({ tg, discord, tgParseMode: "HTML" }, channel);
 };
 
 export const proposalUpdated = (
-  proposal: ProposalDetail | undefined,
+  proposal: ProposalDetail,
   blockId: number,
   sendMessage: Send,
   channel: any
 ): void => {
   if (!proposal) return;
-  const { id, finalizedAt, message, parameters, result, stage } = proposal;
+  const { id, finalizedAt, message, result, stage } = proposal;
+  const { gracePeriod } = JSON.parse(proposal.parameters);
   const link = `${domain}/#/proposals/${id}`;
   if (stage === "Finalized") {
     let label: string = result.toLowerCase();
     let grace = ``;
     if (result === "Approved") {
-      const executesAt = parameters.gracePeriod.toNumber();
+      const executesAt = gracePeriod;
       label = executesAt ? "approved" : "executed";
       if (executesAt && blockId < executesAt)
         grace = `and executes at block ${executesAt}`;
@@ -359,7 +327,7 @@ const getAverage = (array: number[]): number =>
   array.reduce((a: number, b: number) => a + b, 0) / array.length;
 
 export const heartbeat = async (
-  api: Api,
+  api: ApiPromise,
   blocks: Block[],
   timePassed: string,
   proposals: Proposals,
@@ -367,9 +335,8 @@ export const heartbeat = async (
   channel: any
 ): Promise<void> => {
   const price = await fetchTokenValue();
-  const storageSize = await fetchStorageSize();
+  const storageSize = `temporarily disabled`; // TODO merge storagesizebot await fetchStorageSize();
   const durations = blocks.map((b) => b.duration);
-  console.log(durations);
   const blocktime = getAverage(durations) / 1000;
 
   const stake = blocks.map((b) => b.stake);
@@ -413,3 +380,39 @@ export const formatProposalMessage = (
   const discord = `**Type**: ${type}\n**Proposer**: ${handle}\n**Title**: ${title}\n**Stage**: ${stage}\n**Result**: ${result}`;
   return { tg, discord };
 };
+
+export const missingProposalVotes = async (
+  proposals: ProposalVotes[],
+  council: Council
+): Promise<{ discord: string; tg: string }> => {
+  const messages = ["", ""]; // discord, telegram
+
+  proposals.map(({ id, title, votes }) => {
+    if (!title) return;
+    const notifyMembers: string[][] = [[], []];
+    const needToVote = council.seats
+      .filter(
+        ({ memberId }) => !votes.find((vote) => vote.memberId === memberId)
+      )
+      .map((consul) => {
+        if (consul.discord) {
+          const who = `${consul.handle}`; // consul.discord.handle
+          if (!notifyMembers[0].includes(who)) notifyMembers[0].push(who);
+        } else if (consul.telegram?.length)
+          notifyMembers[1].push(consul.telegram);
+        else notifyMembers[0].push(consul.handle);
+      });
+    const selected = [notifyMembers[0].join(" "), notifyMembers[1].join(" ")];
+    if (selected[0].length)
+      messages[0] += `- ${id} **${title}** ${selected[0]} ${domain}/#/proposals/${id}\n`;
+    if (selected[1].length)
+      messages[1] += `- ${id} <a href="${domain}/#/proposals/${id}}">${title}</a>: ${selected[1]}\n`;
+  });
+
+  console.log(messages[0]);
+  const prefix = `*Active Proposals*\n`;
+  return {
+    tg: messages[1].length ? messages[1] : ``,
+    discord: messages[0].length ? prefix + messages[0] : ``,
+  };
+};

+ 91 - 37
scripts/joystreamtelegrambot/src/bot.ts

@@ -15,16 +15,32 @@ import {
 } from "../config";
 
 // types
-import { Block, Council, Options, Proposals, ProposalDetail } from "./types";
+import { Block, Council, Options, Proposals, ProposalVotes } from "./types";
+import { ProposalDetail } from "./lib/types";
 import { types } from "@joystream/types";
+import { ProposalId } from "@joystream/types/proposals";
 import { ApiPromise, WsProvider } from "@polkadot/api";
 import { AccountId, Header, EventRecord } from "@polkadot/types/interfaces";
 
 // functions
-import { getEvents, getBlockHash, getProposalPost } from "./joystream-lib/api";
-import * as announce from "./lib/announcements";
-import * as get from "./lib/getters";
-import { parseArgs, printStatus, passedTime } from "./lib/util";
+import {
+  getActiveProposals,
+  getProposal,
+  getProposalInfo,
+  getBestHash,
+  getBlockHash,
+  getTimestamp,
+  getCouncil,
+  getEvents,
+  getMember,
+  getNextPost,
+  getNextThread,
+  getProposalCount,
+  getProposalPost,
+  getProposalVotes,
+} from "./lib/api";
+import * as announce from "./announcements";
+import { getCouncilHandles, parseArgs, printStatus, passedTime } from "./util";
 import moment from "moment";
 
 const opts: Options = parseArgs(process.argv.slice(2));
@@ -102,7 +118,7 @@ const sendMessage = (
   if (msg.tg.length) sendTelegram(msg.tg, msg.tgParseMode);
 };
 const sendTelegram = (msg: string, tgParseMode: ParseMode | undefined) => {
-  if (bot) {
+  if (bot && msg.length) {
     try {
       bot.sendMessage(chatid, msg, { parse_mode: tgParseMode || "HTML" });
     } catch (e) {
@@ -114,16 +130,49 @@ const sendDiscord = (msg: string, channel: any) => {
   if (!channel) return;
   try {
     channel.send(msg);
-  } catch (e) {
+  } catch (e: any) {
     console.log(`Failed to send to discord: ${e.message}`);
   }
 };
 
+const missingVotesMessages = async (api: ApiPromise, council: Council) => {
+  const active = await getActiveProposals(api);
+  const proposals = await Promise.all(
+    active.map((id) =>
+      getProposalInfo(api, id as unknown as ProposalId).then(({ title }) =>
+        getProposalVotes(api, id).then((votes) => {
+          return { id, title: String(title.toHuman()), votes };
+        })
+      )
+    )
+  );
+  return announce.missingProposalVotes(proposals, council);
+};
+
 const main = async () => {
   const provider = new WsProvider(wsLocation);
   const api = await ApiPromise.create({ provider, types });
   await api.isReady;
 
+  let council: Council = { round: 0, last: "", seats: [] };
+  council.seats = await getCouncilHandles(api);
+
+  client.on("message", (msg): void => {
+    const user = msg.author.id;
+    if (msg.content === "/proposals") {
+      msg
+        .reply(`Checking..`)
+        .then(async (reply) =>
+          reply.edit(
+            await missingVotesMessages(api, council).then(
+              ({ discord }) => discord || `No active proposals.`
+            )
+          )
+        )
+        .catch((error) => console.log(`Discord /proposals: ${error.message}`));
+    }
+  });
+
   const [chain, node, version] = await Promise.all([
     String(await api.rpc.system.chain()),
     api.rpc.system.name(),
@@ -131,10 +180,9 @@ const main = async () => {
   ]);
   log(`Subscribed to ${chain} on ${node} v${version}`);
 
-  let council: Council = { round: 0, last: "" };
   let blocks: Block[] = [];
   let lastEra = 0;
-  let timestamp = await get.timestamp(api);
+  let timestamp = await getTimestamp(api, await getBestHash(api));
   let duration = 0;
   let lastHeartbeat = timestamp;
   let lastCouncilHeartbeat = timestamp;
@@ -162,14 +210,15 @@ const main = async () => {
   let lastProposalUpdate = 0;
 
   if (opts.forum) {
-    posts[0] = await get.currentPostId(api);
-    threads[0] = await get.currentThreadId(api);
+    const hash = await getBestHash(api);
+    posts[0] = (await getNextPost(api, hash)) - 1;
+    threads[0] = (await getNextThread(api, hash)) - 1;
   }
 
   if (opts.proposals) {
     console.log(`updating proposals`);
-    proposals.last = await get.proposalCount(api);
-    proposals.active = await get.activeProposals(api, proposals.last);
+    proposals.last = await getProposalCount(api);
+    proposals.active = await getActiveProposals(api);
   }
 
   const getReward = async (era: number) =>
@@ -178,12 +227,11 @@ const main = async () => {
   api.rpc.chain.subscribeNewHeads(async (header: Header): Promise<void> => {
     // current block
     const id = header.number.toNumber();
-    const hash = await getBlockHash(api, id);
-    const events: EventRecord[] = await getEvents(api, hash);
-
     if (lastBlock.id === id) return;
-    timestamp = await get.timestamp(api);
+    const hash = await getBlockHash(api, id);
+    timestamp = await getTimestamp(api, hash);
     duration = lastBlock.timestamp ? timestamp - lastBlock.timestamp : 0;
+    const events: EventRecord[] = await getEvents(api, hash);
 
     // update validators and nominators every era
     const era = Number(await api.query.staking.currentEra());
@@ -221,6 +269,16 @@ const main = async () => {
     };
     if (duration) blocks = blocks.concat(block);
 
+    // send proposal reminder
+    if (timestamp > lastProposalUpdate + heartbeat) {
+      const msg = await missingVotesMessages(api, council);
+      const channel = await findDiscordChannel("proposals");
+      console.log(msg);
+      //sendMessage(msg, channel);
+      //sendDiscord("Please vote:\n" + msg.discord, channel);
+      lastProposalUpdate = timestamp;
+    }
+
     // heartbeat
     if (timestamp > lastHeartbeat + heartbeat) {
       const time = passedTime(lastHeartbeat, timestamp);
@@ -260,18 +318,16 @@ const main = async () => {
       if (created.length) {
         console.log(
           `proposal created`,
-          created.map((e) => e.toHuman())
+          created.map(({ event }) => event.data)
         );
         created.map(({ event }) =>
-          get
-            .proposalDetail(api, Number(event.data[1]))
-            .then((proposal: ProposalDetail | undefined) =>
-              announce.proposalCreated(
-                proposal,
-                sendMessage,
-                discordChannels.proposals
-              )
+          getProposal(api, event.data[1] as ProposalId).then((proposal) =>
+            announce.proposalCreated(
+              proposal,
+              sendMessage,
+              discordChannels.proposals
             )
+          )
         );
       }
 
@@ -289,22 +345,20 @@ const main = async () => {
           const proposalId = Number(event.data[0]);
           if (seen.includes(proposalId)) return;
           seen.push(proposalId);
-          get
-            .proposalDetail(api, proposalId)
-            .then((proposal: ProposalDetail | undefined) =>
-              announce.proposalUpdated(
-                proposal,
-                id,
-                sendMessage,
-                discordChannels.proposals
-              )
-            );
+          getProposal(api, event.data[0] as ProposalId).then((proposal) =>
+            announce.proposalUpdated(
+              proposal,
+              id,
+              sendMessage,
+              discordChannels.proposals
+            )
+          );
         });
       }
     }
 
     if (opts.forum) {
-      posts[1] = await get.currentPostId(api);
+      posts[1] = (await getNextPost(api, hash)) - 1;
       announce.posts(api, posts, sendMessage, discordChannels.forum);
       posts[0] = posts[1];
     }

+ 1 - 0
scripts/joystreamtelegrambot/src/lib

@@ -0,0 +1 @@
+Subproject commit 3d35ae5fe9931fd63e21cbbc25380719dccecb94

+ 0 - 152
scripts/joystreamtelegrambot/src/lib/getters.ts

@@ -1,152 +0,0 @@
-import { formatProposalMessage } from "./announcements";
-import axios from "axios";
-
-//types
-
-import { Api, ProposalArray, ProposalDetail } from "../types";
-import {
-  ChannelId,
-  PostId,
-  ProposalDetailsOf,
-  ThreadId,
-} from "@joystream/types/augment";
-import { Category, CategoryId } from "@joystream/types/forum";
-import { MemberId, Membership } from "@joystream/types/members";
-import { Proposal } from "@joystream/types/proposals";
-
-// api
-
-export const timestamp = async (api: Api) =>
-  (await api.query.timestamp.now()).toNumber();
-
-export const memberHandle = async (api: Api, id: MemberId): Promise<string> => {
-  const membership: Membership = await api.query.members.membershipById(id);
-  return membership.handle.toJSON();
-};
-
-export const memberHandleByAccount = async (
-  api: Api,
-  account: string
-): Promise<string> => {
-  const id: MemberId = await api.query.members.memberIdsByRootAccountId(
-    account
-  );
-  const handle: string = await memberHandle(api, id);
-  return handle;
-};
-
-// forum
-
-export const categoryById = async (api: Api, id: number): Promise<Category> => {
-  const category: Category = await api.query.forum.categoryById(id);
-  return category;
-};
-
-export const currentPostId = async (api: Api): Promise<number> => {
-  const postId: PostId = await api.query.forum.nextPostId();
-  return postId.toNumber() - 1;
-};
-
-export const currentThreadId = async (api: Api): Promise<number> => {
-  const threadId: ThreadId = await api.query.forum.nextThreadId();
-  return threadId.toNumber() - 1;
-};
-
-export const currentCategoryId = async (api: Api): Promise<number> => {
-  const categoryId: CategoryId = await api.query.forum.nextCategoryId();
-  return categoryId.toNumber() - 1;
-};
-
-// proposals
-
-export const proposalCount = async (api: Api): Promise<number> =>
-  Number(await api.query.proposalsEngine.proposalCount());
-
-export const activeProposals = async (
-  api: Api,
-  last: number
-): Promise<number[]> => {
-  let count = 0;
-  try {
-    console.log(`fetching active proposal count`);
-    count = Number(await api.query.proposalsEngine.activeProposalCount());
-  } catch (e) {
-    console.error(`failed to fetch active proposal count: ${e.message}`);
-  }
-  let ids: number[] = [];
-  for (let id = last; ids.length < count; id--) {
-    if ([837, 839].includes(id)) continue;
-    try {
-      console.log(`fetching proposal ${id}`);
-      const proposal = await proposalDetail(api, id);
-      if (proposal && proposal.result === "Pending") ids.push(id);
-    } catch (e) {
-      console.error(`Failed to fetch proposal ${id}: ${e.message}`);
-    }
-  }
-  return ids;
-};
-
-const getProposalType = async (api: Api, id: number): Promise<string> => {
-  const details: ProposalDetailsOf =
-    await api.query.proposalsCodex.proposalDetailsByProposalId(id);
-  const [type]: string[] = Object.getOwnPropertyNames(details.toJSON());
-  return type;
-};
-
-export const proposalDetail = async (
-  api: Api,
-  id: number
-): Promise<ProposalDetail | undefined> => {
-  let proposal: Proposal;
-  try {
-    proposal = await api.query.proposalsEngine.proposals(id);
-    if (!proposal) return;
-  } catch (e) {
-    console.log(`Failed to fetch proposal detail ${id}`);
-    return;
-  }
-  const status: { [key: string]: any } = proposal.status;
-  if (!status) return;
-  const stage: string = status.isActive ? "Active" : "Finalized";
-  const { finalizedAt, proposalStatus } = status[`as${stage}`];
-  const result: string = proposalStatus
-    ? (proposalStatus.isApproved && "Approved") ||
-      (proposalStatus.isCanceled && "Canceled") ||
-      (proposalStatus.isExpired && "Expired") ||
-      (proposalStatus.isRejected && "Rejected") ||
-      (proposalStatus.isSlashed && "Slashed") ||
-      (proposalStatus.isVetoed && "Vetoed")
-    : "Pending";
-  const { parameters, proposerId } = proposal;
-  const author: string = await memberHandle(api, proposerId);
-  const title: string = proposal.title.toString();
-  const type: string = await getProposalType(api, id);
-  const args: string[] = [String(id), title, type, stage, result, author];
-  const message: { tg: string; discord: string } = formatProposalMessage(args);
-  const createdAt: number = proposal.createdAt.toNumber();
-  return { id, createdAt, finalizedAt, parameters, message, stage, result };
-};
-
-// status endpoint
-
-export const fetchTokenValue = async (): Promise<string> =>
-  axios
-    .get("https://status.joystream.org/status")
-    .then(({ data }) => `${Math.floor(+data.price * 100000000) / 100} $`)
-    .catch((e) => {
-      console.log(`Failed to fetch status.`);
-      return `?`;
-    });
-
-// hdyra
-
-export const fetchStorageSize = async () => {
-  const dashboard = "https://analytics.dapplooker.com/api/public/dashboard";
-  const asset = "c70b56bd-09a0-4472-a557-796afdc64d3b/card/155";
-
-  const { data } = await axios.get(`${dashboard}/${asset}`);
-
-  const size = Math.round(data.data.rows[0][0]) + " GB";
-  return size;
-};

+ 0 - 57
scripts/joystreamtelegrambot/src/lib/util.ts

@@ -1,57 +0,0 @@
-import { Api, Options, Proposals } from "../types";
-import moment from "moment";
-
-export const parseArgs = (args: string[]): Options => {
-  const inArgs = (term: string): boolean => {
-    return args.find((a) => a.search(term) > -1) ? true : false;
-  };
-
-  const options: Options = {
-    verbose: inArgs("--verbose") ? 2 : inArgs("--quiet") ? 0 : 1,
-    channel: inArgs("--channel"),
-    council: inArgs("--council"),
-    forum: inArgs("--forum"),
-    proposals: inArgs("--proposals"),
-  };
-
-  if (options.verbose > 1) console.debug("args", args, "\noptions", options);
-  return options;
-};
-
-export const printStatus = (
-  opts: Options,
-  data: {
-    block: number;
-    chain: string;
-    posts: number[];
-    proposals: Proposals;
-  }
-): void => {
-  if (opts.verbose < 1) return;
-
-  const { block, chain, proposals, posts } = data;
-  const date = formatTime();
-  let message = `[${date}] Chain:${chain} Block:${block} `;
-
-  if (opts.forum) message += `Post:${posts[1]} `;
-
-  if (opts.proposals)
-    message += `Proposals:${proposals.current} (Active:${proposals.active.length} Pending:${proposals.executing.length}) `;
-
-  console.log(message);
-};
-
-// time
-export const formatTime = (time?: any, format = "H:mm:ss"): string =>
-  moment(time).format(format);
-
-export const passedTime = (start: number, now: number): string => {
-  const passed = moment.utc(moment(now).diff(start)).valueOf();
-  const format =
-    passed > 86400000
-      ? "d:HH:mm:ss[d]"
-      : passed > 3600000
-      ? "H:mm:ss[h]"
-      : "mm:ss[m]";
-  return formatTime(passed, format);
-};

+ 4 - 24
scripts/joystreamtelegrambot/src/tests.ts

@@ -1,5 +1,7 @@
 //import TelegramBot from "node-telegram-bot-api";
+import * as announce from "./announcements";
 import { wsLocation } from "../config";
+import { getCouncilHandles } from "./util";
 
 // types
 import { Council, Send } from "./types";
@@ -7,10 +9,6 @@ import { types } from "@joystream/types";
 import { ApiPromise, WsProvider } from "@polkadot/api";
 import { Header } from "@polkadot/types/interfaces";
 
-// functions
-import * as announce from "./lib/announcements";
-import * as get from "./lib/getters";
-
 const log = (msg: string) => console.log(msg);
 const sendMessage: Send = (msg, channel) => console.log(msg.discord);
 const nochan = {};
@@ -27,11 +25,9 @@ const main = async () => {
   ]);
   log(`Connected to ${chain} on ${node} v${version}`);
 
-  let council: Council = { round: 0, last: "" };
+  let council: Council = { round: 0, last: "", seats: [] };
+  council.seats = await getCouncilHandles(api);
   let lastBlock: number = 0;
-  let categories = [0, 0];
-  let posts = [0, 0];
-  let channels = [0, 0];
 
   const unsubscribe = await api.rpc.chain.subscribeNewHeads(
     async (block: Header): Promise<void> => {
@@ -48,22 +44,6 @@ const main = async () => {
         nochan
       );
       lastBlock = currentBlock;
-
-      log("first category");
-      announce.categories(api, categories, sendMessage, nochan);
-
-      log("last category");
-      categories[1] = await get.currentCategoryId(api);
-      categories[0] = categories[1] - 1;
-      announce.categories(api, categories, sendMessage, nochan);
-
-      log("first post");
-      announce.posts(api, posts, sendMessage, nochan);
-
-      log("last post");
-      posts[1] = await get.currentPostId(api);
-      posts[0] = posts[1] - 1;
-      announce.posts(api, posts, sendMessage, nochan);
     }
   );
 };

+ 19 - 23
scripts/joystreamtelegrambot/src/types/index.ts → scripts/joystreamtelegrambot/src/types.ts

@@ -1,18 +1,11 @@
-import { ApiPromise } from "@polkadot/api";
 import { MemberId } from "@joystream/types/members";
-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";
+import { Post } from "@joystream/types/forum";
 import { ParseMode } from "node-telegram-bot-api";
 
-export interface Api {
-  query: any;
-}
-
 export interface Council {
   round: number;
   last: string;
+  seats: MemberHandles[];
 }
 
 export interface Options {
@@ -23,23 +16,11 @@ export interface Options {
   proposals: boolean;
 }
 
-export interface ProposalDetail {
-  id: number;
-  createdAt: number;
-  finalizedAt: number;
-  message: { tg: string; discord: string };
-  parameters: ProposalParameters;
-  stage: string;
-  result: string;
-}
-
-export type ProposalArray = number[];
-
 export interface Proposals {
   current: number;
   last: number;
-  active: ProposalArray;
-  executing: ProposalArray;
+  active: ProposalId[];
+  executing: number[];
 }
 
 export interface Member {
@@ -63,3 +44,18 @@ export type Send = (
   msg: { tg: string; discord: string; tgParseMode: ParseMode | undefined },
   channel: any
 ) => void;
+
+// github
+export interface MemberHandles {
+  memberId: MemberId;
+  handle: string;
+  discord?: { handle: string; id: number };
+  telegram?: string;
+}
+
+// jstats
+export interface ProposalVotes {
+  id: ProposalId;
+  title: string;
+  votes: { memberId: MemberId; vote: string }[];
+}

+ 129 - 0
scripts/joystreamtelegrambot/src/util.ts

@@ -0,0 +1,129 @@
+import { apiUrl, statusEndpoint } from "../config";
+import { Options, Proposals, MemberHandles, ProposalVotes } from "./types";
+import { getCouncil, getMemberHandle, getMemberIdByAccount } from "./lib/api";
+import moment from "moment";
+import axios from "axios";
+
+//types
+import { ApiPromise } from "@polkadot/api";
+import { AccountId } from "@polkadot/types/interfaces";
+
+export const parseArgs = (args: string[]): Options => {
+  const inArgs = (term: string): boolean => {
+    return args.find((a) => a.search(term) > -1) ? true : false;
+  };
+
+  const options: Options = {
+    verbose: inArgs("--verbose") ? 2 : inArgs("--quiet") ? 0 : 1,
+    channel: inArgs("--channel"),
+    council: inArgs("--council"),
+    forum: inArgs("--forum"),
+    proposals: inArgs("--proposals"),
+  };
+
+  if (options.verbose > 1) console.debug("args", args, "\noptions", options);
+  return options;
+};
+
+export const printStatus = (
+  opts: Options,
+  data: {
+    block: number;
+    chain: string;
+    posts: number[];
+    proposals: Proposals;
+  }
+): void => {
+  if (opts.verbose < 1) return;
+
+  const { block, chain, proposals, posts } = data;
+  const date = formatTime();
+  let message = `[${date}] Chain:${chain} Block:${block} `;
+
+  if (opts.forum) message += `Post:${posts[1]} `;
+
+  if (opts.proposals)
+    message += `Proposals:${proposals.current} (Active:${proposals.active.length} Pending:${proposals.executing.length}) `;
+
+  console.log(message);
+};
+
+// time
+export const formatTime = (time?: any, format = "H:mm:ss"): string =>
+  moment(time).format(format);
+
+export const passedTime = (start: number, now: number): string => {
+  const passed = moment.utc(moment(now).diff(start)).valueOf();
+  const format =
+    passed > 86400000
+      ? "d:HH:mm:ss[d]"
+      : passed > 3600000
+      ? "H:mm:ss[h]"
+      : "mm:ss[m]";
+  return formatTime(passed, format);
+};
+
+// status endpoint
+const formatPrice = (price: number) =>
+  `${Math.floor(price * 100000000) / 100} $`;
+
+export const fetchTokenValue = (): Promise<string> =>
+  axios
+    .get(statusEndpoint[0])
+    .then(({ data }) => formatPrice(+data.price))
+    .catch(async () => {
+      const { data } = await axios.get(statusEndpoint[1]);
+      if (data && !data.error) return formatPrice(+data.price);
+      console.log(`Failed to fetch status.`);
+      return `?`;
+    });
+
+// member handles (tg, discord, github)
+export const getMemberHandles = async (): Promise<MemberHandles[]> => {
+  console.debug(`Fetching user handles`);
+  const rawUrl = `https://raw.githubusercontent.com/Joystream/community-repo/master/council/council_member_discord_usernames.md`;
+  return await axios
+    .get(rawUrl)
+    .then(({ data }) => {
+      const rows = data
+        .split(`\n`)
+        .filter((line: string) => line.includes(`|`))
+        .slice(2);
+      return rows.map((row: string) => {
+        const [, handle, discord, id, telegram, github] = row
+          .split(`|`)
+          .map((s) => s.trim());
+        return { handle, discord: { handle: discord, id }, telegram, github };
+      });
+    })
+    .catch((error) => console.error(`Fetch user handles.`, error.message));
+};
+
+// find handle from account
+export const getMemberHandlesByAccount = async (
+  api: ApiPromise,
+  handles: MemberHandles[],
+  account: AccountId
+): Promise<MemberHandles> => {
+  const memberId = await getMemberIdByAccount(api, account);
+  const handle = await getMemberHandle(api, memberId);
+  const member = handles.find((m) => m.handle === handle);
+  return { ...member, handle, memberId };
+};
+
+export const getCouncilHandles = async (
+  api: ApiPromise
+): Promise<MemberHandles[]> => {
+  const seats = await getCouncil(api);
+  const handles: MemberHandles[] = await getMemberHandles();
+  return await Promise.all(
+    seats.map((seat) => getMemberHandlesByAccount(api, handles, seat.member))
+  );
+};
+
+// jstats
+export const getProposals = async (): Promise<ProposalVotes[]> => {
+  console.debug(`Fetching proposals`);
+  const { data } = await axios.get(apiUrl + `/v1/proposals`);
+  return data;
+};

+ 1 - 1
scripts/report-generator/src/lib

@@ -1 +1 @@
-Subproject commit dd6872ca55db450d6ea4ccf869392d26d6d9774c
+Subproject commit 978eba184d771bfa6aa80ca1f1756a718dcd58da