123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- import { suppressedThreads } from "../../config";
- import {
- Api,
- Block,
- Council,
- Member,
- ProposalDetail,
- Proposals,
- Send,
- } from "../types";
- 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 {
- categoryById,
- memberHandle,
- memberHandleByAccount,
- proposalDetail,
- fetchTokenValue,
- fetchStorageSize,
- } from "./getters";
- import moment, { now } from "moment";
- const dateFormat = "DD-MM-YYYY HH:mm (UTC)";
- 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++) {
- if (result[test] !== "") return result;
- result = await cb();
- await sleep(5000);
- }
- };
- // announce latest channels
- export const channels = async (
- api: Api,
- channels: number[],
- sendMessage: Send,
- channel: any
- ): Promise<void> => {
- const [last, current] = channels;
- const messages: string[][] = [[], []];
- for (let id: number = +last + 1; id <= current; id++) {
- const channel: Channel = await query("title", () =>
- api.query.contentWorkingGroup.channelById(id)
- );
- const member: Member = { id: channel.owner.asMember, handle: "", url: "" };
- member.handle = await memberHandle(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>`
- );
- messages[1].push(
- `**Channel ${id}** by ${member.handle} (${member.id})\n${domain}/#//media/channels/${id}`
- );
- }
- sendMessage(
- {
- tg: messages[0].join("\r\n\r\n"),
- discord: messages[1].join(`\n\n`),
- tgParseMode: "HTML",
- },
- channel
- );
- };
- // announce council change
- export const council = async (
- api: Api,
- 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 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`;
- }
- } else {
- const remainingBlocks = stage.toJSON()[stageString] - currentBlock;
- const m = moment().add(remainingBlocks * 6, "second");
- const endDate = formatTime(m, dateFormat);
- const link = `${domain}/#/council/`;
- if (stageString === "announcing") {
- msg[0] = `Council election started. You can <b><a href="${link}applicants">announce your application</a></b> until ${endDate}`;
- msg[1] = `Council election started. You can **announce your application** until ${endDate} ${link}applicants`;
- } else if (stageString === "voting") {
- msg[0] = `Council election: <b><a href="${link}applicants">Vote</a></b> until ${endDate}`;
- msg[1] = `Council election: **Vote* until ${endDate} ${link}applicants`;
- } else if (stageString === "revealing") {
- msg[0] = `Council election: <b><a href="${link}votes">Reveal your votes</a></b> until ${endDate}`;
- msg[1] = `Council election: **Reveal your votes** until ${endDate} ${link}votes`;
- }
- }
- if (
- council.last !== "" &&
- round !== council.round &&
- stageString !== council.last
- ) {
- sendMessage({ tg: msg[0], discord: msg[1], tgParseMode: "HTML" }, channel);
- }
- return { round, last: stageString };
- };
- export const councilStatus = async (
- api: Api,
- 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 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");
- }
- } else {
- const remainingBlocks = stage.toJSON()[stageString] - currentBlock;
- stageEndDate = moment().add(remainingBlocks * 6, "second");
- }
- const revealingEndsAt =
- councilTermEndBlock + announcingPeriod + votingPeriod + revealingPeriod;
- const termBlocksRemaining = revealingEndsAt - currentBlock;
- let councilEndDate = moment().add(termBlocksRemaining * 6, "seconds");
- let councilEndDateString = formatTime(councilEndDate, dateFormat);
- let councilDaysLeft = councilEndDate.diff(moment(), "d");
- let councilDurationSuffix = "day(s)";
- if (councilDaysLeft <= 0) {
- councilDaysLeft = councilEndDate.diff(moment(), "h");
- councilDurationSuffix = "hour(s)";
- }
- if (councilDaysLeft <= 0) {
- councilDaysLeft = councilEndDate.diff(moment(), "m");
- councilDurationSuffix = "minute(s)";
- }
- let stageEndDateString = formatTime(stageEndDate, dateFormat);
- let stageDaysLeft = stageEndDate.diff(moment(), "d");
- let stageDurationSuffix = "day(s)";
- if (stageDaysLeft <= 0) {
- stageDaysLeft = stageEndDate.diff(moment(), "h");
- stageDurationSuffix = "hour(s)";
- }
- if (stageDaysLeft <= 0) {
- stageDaysLeft = stageEndDate.diff(moment(), "m");
- stageDurationSuffix = "minute(s)";
- }
- const msgTg = `It is block number *#${currentBlock}* \nCouncil ends in *${councilDaysLeft} ${councilDurationSuffix}* on *${councilEndDateString}* \nCurrent stage *${stageString}* ends in *${stageDaysLeft} ${stageDurationSuffix}* on *${stageEndDateString}*.`;
- const msgDs = `It is block number **#${currentBlock}** \nCouncil ends in **${councilDaysLeft} ${councilDurationSuffix}** on *${councilEndDateString}* \nCurrent stage **${stageString}** ends in *${stageDaysLeft} ${stageDurationSuffix}* on *${stageEndDateString}*.`;
- sendMessage({ tg: msgTg, discord: msgDs, tgParseMode: "Markdown" }, channel);
- };
- // forum
- // announce latest categories
- export const categories = async (
- api: Api,
- category: number[],
- sendMessage: Send,
- channel: any
- ): Promise<number> => {
- if (category[0] === category[1]) return category[0];
- const messages: string[][] = [[], []];
- for (let id: number = +category[0] + 1; id <= category[1]; id++) {
- const cat: Category = await query("title", () => categoryById(api, id));
- messages[0].push(
- `Category ${id}: <b><a href="${domain}/#/forum/categories/${id}">${cat.title}</a></b>`
- );
- messages[1].push(
- `Category ${id}: **${cat.title}** ${domain}/#/forum/categories/${id}`
- );
- }
- sendMessage(
- {
- tg: messages[0].join("\r\n\r\n"),
- discord: messages[1].join(`\n\n`),
- tgParseMode: "HTML",
- },
- channel
- );
- return category[1];
- };
- // announce latest posts
- export const posts = async (
- api: Api,
- posts: number[],
- sendMessage: Send,
- channel: any
- ): Promise<number> => {
- const [last, current] = posts;
- if (current === last) return last;
- 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 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
- if (suppressedThreads.includes(threadId)) continue;
- const category: Category = await query("title", () =>
- categoryById(api, categoryId)
- );
- const handle = await memberHandleByAccount(api, post.author_id.toJSON());
- const s = {
- content: post.current_text.substring(0, 250),
- link: `${domain}/#/forum/threads/${threadId}?replyIdx=${replyId}`,
- };
- messages[0].push(
- `<u><a href="${domain}/#/forum/categories/${category.id}">${category.title}</a></u> <b><a href="${domain}/#/members/${handle}">${handle}</a></b> posted in <b><a href="${domain}/#/forum/threads/${threadId}?replyIdx=${replyId}">${thread.title}</a></b>:\n\r<i>${s.content}</i> <a href="${s.link}">more</a>`
- );
- messages[1].push(
- `**[${category.title}]** ${handle} posted in **${thread.title}**:\n*${s.content}*\nMore: ${s.link}`
- );
- }
- sendMessage(
- {
- tg: messages[0].join("\r\n\r\n"),
- discord: messages[1].join(`\n\n`),
- tgParseMode: "HTML",
- },
- channel
- );
- return current;
- };
- export const proposalCreated = (
- proposal: ProposalDetail,
- sendMessage: Send,
- channel: any
- ): 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);
- };
- 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}`;
- }
- // 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);
- }
- };
- 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
- const getAverage = (array: number[]): number =>
- array.reduce((a: number, b: number) => a + b, 0) / array.length;
- export const heartbeat = async (
- api: Api,
- blocks: Block[],
- timePassed: string,
- proposals: Proposals,
- sendMessage: Send,
- channel: any
- ): Promise<void> => {
- const price = await fetchTokenValue();
- const storageSize = await fetchStorageSize();
- const durations = blocks.map((b) => b.duration);
- console.log(durations);
- 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);
- 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();
- const pending = proposals.active.length;
- const finalized = proposals.executing.length;
- const p = (n: number) => (n > 1 ? "proposals" : "proposal");
- let proposalString: string[] = pending
- ? [
- `<a href="${domain}/#/proposals">${pending} pending ${p(pending)}</a> `,
- `${pending} active ${p(pending)} ${domain}/#/proposals`,
- ]
- : ["", ""];
- if (finalized)
- proposalString = proposalString.map(
- (s) => (s += `${finalized} ${p(finalized)} in grace period.`)
- );
- const msg = ` ${blocks.length} blocks produced in ${timePassed}
- Blocktime: ${blocktime.toFixed(3)}s
- Price: ${price} / 1 M tJOY
- Stake: ${avgStake.toFixed(1)} / ${avgIssued.toFixed()} M tJOY (${percent}%)
- Validators: ${avgVals.toFixed()} (${reward} tJOY/h)
- Nominators: ${getAverage(noms).toFixed()}
- Volume: ${storageSize}\n`;
- const tg = msg + proposalString[0];
- const discord = msg + proposalString[1];
- sendMessage({ tg, discord, tgParseMode: "HTML" }, channel);
- };
- export const formatProposalMessage = (
- data: string[]
- ): { tg: string; discord: string } => {
- const [id, title, type, stage, result, handle] = data;
- const tg = `<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}`;
- const discord = `**Type**: ${type}\n**Proposer**: ${handle}\n**Title**: ${title}\n**Stage**: ${stage}\n**Result**: ${result}`;
- return { tg, discord };
- };
|