فهرست منبع

read proposal updates from block events, simplify tests

Joystream Stats 3 سال پیش
والد
کامیت
0ee085556d

+ 69 - 12
scripts/joystreamtelegrambot/src/bot.ts

@@ -4,6 +4,7 @@ import TelegramBot, {
   SendMessageOptions,
 } from "node-telegram-bot-api";
 import {
+  domain,
   discordToken,
   tgToken,
   chatid,
@@ -14,12 +15,13 @@ import {
 } from "../config";
 
 // types
-import { Block, Council, Options, Proposals } from "./types";
+import { Block, Council, Options, Proposals, ProposalDetail } from "./types";
 import { types } from "@joystream/types";
 import { ApiPromise, WsProvider } from "@polkadot/api";
-import { AccountId, Header } from "@polkadot/types/interfaces";
+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";
@@ -44,8 +46,26 @@ client.on("ready", async () => {
   discordChannels.proposals = await findDiscordChannel("proposals-bot");
   discordChannels.forum = await findDiscordChannel("forum-bot");
   discordChannels.tokenomics = await findDiscordChannel("tokenomics");
+
+  deleteDuplicateMessages(discordChannels.proposals);
 });
 
+const deleteDuplicateMessages = (channel: any) => {
+  let messages: { [key: string]: any } = {};
+  channel.messages.fetch({ limit: 100 }).then((msgs: any) =>
+    msgs.map((msg: any) => {
+      const txt = msg.content.slice(0, 100);
+      if (messages[txt]) {
+        if (msg.deleted) console.log(`duplicate msg already deleted`);
+        else {
+          console.log(`deleting duplicate message`, msg.content);
+          msg.delete().then(() => console.log(`deleted message ${msg.id}`));
+        }
+      } else messages[txt] = msg;
+    })
+  );
+};
+
 const findDiscordChannel = (name: string) =>
   client.channels.cache.find((channel: any) => channel.name === name);
 
@@ -160,6 +180,8 @@ 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);
@@ -233,18 +255,53 @@ const main = async () => {
     }
 
     if (opts.proposals) {
-      proposals.current = await get.proposalCount(api);
+      // new proposal
+      const created = events.filter(
+        ({ event }: EventRecord) => event.method === "ProposalCreated"
+      );
+      if (created.length) {
+        console.log(
+          `proposal created`,
+          created.map((e) => e.toHuman())
+        );
+        created.map(({ event }) =>
+          get
+            .proposalDetail(api, Number(event.data[1]))
+            .then((proposal: ProposalDetail) =>
+              announce.proposalCreated(
+                proposal,
+                sendMessage,
+                discordChannels.proposals
+              )
+            )
+        );
+      }
 
-      if (proposals.current > proposals.last && !announced[proposals.current]) {
-        announced[`proposal${proposals.current}`] = true;
-        proposals = await announce.proposals(
-          api,
-          proposals,
-          id,
-          sendMessage,
-          discordChannels.proposals
+      // status update
+      const updated = events.filter(
+        ({ event }: EventRecord) => event.method === "ProposalStatusUpdated"
+      );
+      let seen: number[] = [];
+      if (updated.length) {
+        console.log(
+          `proposal update`,
+          updated.map((e) => e.toHuman())
         );
-        lastProposalUpdate = timestamp;
+        updated.map(({ event }) => {
+          const proposalId = Number(event.data[0]);
+          if (seen.includes(proposalId)) return;
+          seen.push(proposalId);
+          get
+            .proposalDetail(api, proposalId)
+            .then((proposal: ProposalDetail) =>
+              announce.proposalUpdated(
+                proposal,
+                id,
+                sendMessage,
+                discordChannels.proposals
+              )
+            );
+        });
       }
     }
 

+ 49 - 48
scripts/joystreamtelegrambot/src/lib/announcements.ts

@@ -11,6 +11,7 @@ import {
 import { BlockNumber } from "@polkadot/types/interfaces";
 import { Channel, ElectionStage } from "@joystream/types/augment";
 import { Category, Thread, Post } from "@joystream/types/forum";
+import { DiscussionPost } from "@joystream/types/proposals";
 import { domain } from "../../config";
 import { formatTime } from "./util";
 import {
@@ -293,61 +294,61 @@ export const posts = async (
   return current;
 };
 
-// announce latest proposals
-export const proposals = async (
-  api: Api,
-  prop: Proposals,
-  block: number,
+export const proposalCreated = (
+  proposal: ProposalDetail,
   sendMessage: Send,
   channel: any
-): 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 endTime = moment()
-      .add(6 * (votingEndsAt - block), "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`;
-    sendMessage({ tg, discord, tgParseMode: "HTML" }, channel);
-    active.push(id);
-  }
+): void => {
+  const { id, createdAt, finalizedAt, message, parameters, result } = proposal;
+  if (!createdAt) return console.warn(`proposalCreated: wrong data`, proposal);
+  const votingEndsAt = createdAt + parameters.votingPeriod.toNumber();
+  const endTime = moment()
+    .add(6 * (votingEndsAt - id), "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`;
+  sendMessage({ tg, discord, tgParseMode: "HTML" }, channel);
+};
 
-  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.toLowerCase();
-      if (result === "Approved") {
-        const executed = parameters.gracePeriod.toNumber() > 0 ? false : true;
-        label = executed ? "executed" : "finalized";
-        if (!executed) executing.push(id);
-      }
-      const link = `${domain}/#/proposals/${id}`;
-      const tg = `<a href="${link}">Proposal ${id}</a> <b>${label}</b> at block ${finalizedAt}.\r\n${message.tg}`;
-      const discord = `Proposal ${id} **${label}** at block ${finalizedAt}.\n${message.discord}\n${link}\n`;
-      sendMessage({ tg, discord, tgParseMode: "HTML" }, channel);
-      active = active.filter((a) => a !== id);
+export const proposalUpdated = (
+  proposal: ProposalDetail,
+  blockId: number,
+  sendMessage: Send,
+  channel: any
+): void => {
+  const { id, finalizedAt, message, parameters, result, stage } = proposal;
+  const link = `${domain}/#/proposals/${id}`;
+  if (stage === "Finalized") {
+    let label: string = result.toLowerCase();
+    let grace = ``;
+    if (result === "Approved") {
+      const executesAt = parameters.gracePeriod.toNumber();
+      label = executesAt ? "approved" : "executed";
+      if (executesAt && blockId < executesAt)
+        grace = `and executes at block ${executesAt}`;
     }
-  }
-
-  for (const id of executing) {
-    const proposal = await proposalDetail(api, id);
-    const { finalizedAt, message, parameters } = proposal;
-    const executesAt = +finalizedAt + parameters.gracePeriod.toNumber();
-    if (block < executesAt) continue;
-    const link = `${domain}/#/proposals/${id}`;
-    const tg = `<a href="${link}">Proposal ${id}</a> <b>executed</b> at block ${executesAt}.\r\n${message.tg}`;
-    const discord = `Proposal ${id} **executed** at block ${executesAt}.\n${message.discord}\n${link}\n`;
+    // send announcement
+    const tg = `<a href="${link}">Proposal ${id}</a> <b>${label}</b> at block ${finalizedAt}${grace}.\r\n${message.tg}`;
+    const discord = `Proposal ${id} **${label}** at block ${finalizedAt}${grace}.\n${message.discord}\n${link}\n`;
     sendMessage({ tg, discord, tgParseMode: "HTML" }, channel);
-    executing = executing.filter((e) => e !== id);
   }
+};
 
-  return { current, last: current, active, executing };
+export const proposalPost = async (
+  post: DiscussionPost,
+  author: string,
+  proposalId: number,
+  sendMessage: Send,
+  channel: any
+) => {
+  const { text, created_at, author_id, thread_id } = post;
+  const txt = text.slice(0, 100);
+  const link = `${domain}/#/proposals/${proposalId}`;
+  const tg = `<b>${author}</b> commented on <b><a href="${link}">Proposal ${proposalId}</a></b>: ${txt}`;
+  const discord = `**${author}** commented on **Proposal ${proposalId}**: ${txt} $link`;
+  console.log(tg);
+  sendMessage({ tg, discord, tgParseMode: "HTML" }, channel);
 };
 
 // heartbeat

+ 1 - 3
scripts/joystreamtelegrambot/src/lib/getters.ts

@@ -98,8 +98,6 @@ 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);
   const title: string = proposal.title.toString();
@@ -107,7 +105,7 @@ export const proposalDetail = async (
   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 { createdAt, finalizedAt, parameters, message, stage, result, exec };
+  return { id, createdAt, finalizedAt, parameters, message, stage, result };
 };
 
 // status endpoint

+ 15 - 30
scripts/joystreamtelegrambot/src/tests.ts

@@ -2,7 +2,7 @@
 import { wsLocation } from "../config";
 
 // types
-import { Council, Proposals } from "./types";
+import { Council, Send } from "./types";
 import { types } from "@joystream/types";
 import { ApiPromise, WsProvider } from "@polkadot/api";
 import { Header } from "@polkadot/types/interfaces";
@@ -11,8 +11,9 @@ import { Header } from "@polkadot/types/interfaces";
 import * as announce from "./lib/announcements";
 import * as get from "./lib/getters";
 
-const log = (msg: string): void => console.log(msg);
-const sendMessage = log;
+const log = (msg: string) => console.log(msg);
+const sendMessage: Send = (msg, channel) => console.log(msg.discord);
+const nochan = {};
 
 const main = async () => {
   const provider = new WsProvider(wsLocation);
@@ -28,12 +29,6 @@ const main = async () => {
 
   let council: Council = { round: 0, last: "" };
   let lastBlock: number = 0;
-  let proposals: Proposals = {
-    last: 1,
-    current: 2,
-    active: [],
-    executing: [],
-  };
   let categories = [0, 0];
   let posts = [0, 0];
   let channels = [0, 0];
@@ -45,40 +40,30 @@ const main = async () => {
       lastBlock = block.number.toNumber();
       const currentBlock = block.number.toNumber();
       log("current council");
-      council = await announce.council(api, council, currentBlock, sendMessage);
+      council = await announce.council(
+        api,
+        council,
+        currentBlock,
+        sendMessage,
+        nochan
+      );
       lastBlock = currentBlock;
 
-      log("first proposal");
-      announce.proposals(api, proposals, lastBlock, sendMessage);
-
-      log("last proposal");
-      proposals.current = await get.proposalCount(api);
-      proposals.last = proposals.current - 1;
-      announce.proposals(api, proposals, lastBlock, sendMessage);
-
       log("first category");
-      announce.categories(api, categories, sendMessage);
+      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);
+      announce.categories(api, categories, sendMessage, nochan);
 
       log("first post");
-      announce.posts(api, posts, sendMessage);
+      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);
-
-      log("first channel");
-      announce.channels(api, channels, sendMessage);
-
-      log("last channel");
-      channels[1] = await get.currentChannelId(api);
-      channels[0] = channels[1] - 1;
-      announce.channels(api, channels, sendMessage);
+      announce.posts(api, posts, sendMessage, nochan);
     }
   );
 };

+ 1 - 2
scripts/joystreamtelegrambot/src/types/index.ts

@@ -1,6 +1,5 @@
 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";
@@ -25,13 +24,13 @@ export interface Options {
 }
 
 export interface ProposalDetail {
+  id: number;
   createdAt: number;
   finalizedAt: number;
   message: { tg: string; discord: string };
   parameters: ProposalParameters;
   stage: string;
   result: string;
-  exec: any;
 }
 
 export type ProposalArray = number[];