announcements.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import {
  2. Api,
  3. Block,
  4. Council,
  5. Member,
  6. ProposalDetail,
  7. Proposals,
  8. } from "../types";
  9. import { BlockNumber } from "@polkadot/types/interfaces";
  10. import { Channel, ElectionStage } from "@joystream/types/augment";
  11. import { Category, Thread, Post } from "@joystream/types/forum";
  12. import { domain } from "../../config";
  13. import { formatTime } from "./util";
  14. import {
  15. categoryById,
  16. memberHandle,
  17. memberHandleByAccount,
  18. proposalDetail,
  19. } from "./getters";
  20. import moment from "moment";
  21. const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
  22. // query API repeatedly to ensure a result
  23. const query = async (test: string, cb: () => Promise<any>): Promise<any> => {
  24. let result = await cb();
  25. for (let i: number = 0; i < 10; i++) {
  26. if (result[test] !== "") return result;
  27. result = await cb();
  28. await sleep(5000);
  29. }
  30. };
  31. // announce latest channels
  32. export const channels = async (
  33. api: Api,
  34. channels: number[],
  35. sendMessage: (msg: string) => void
  36. ): Promise<number> => {
  37. const [last, current] = channels;
  38. const messages: string[] = [];
  39. for (let id: number = +last + 1; id <= current; id++) {
  40. const channel: Channel = await query("title", () =>
  41. api.query.contentWorkingGroup.channelById(id)
  42. );
  43. const member: Member = { id: channel.owner, handle: "", url: "" };
  44. member.handle = await memberHandle(api, member.id.toJSON());
  45. member.url = `${domain}/#/members/${member.handle}`;
  46. messages.push(
  47. `<b>Channel <a href="${domain}/#//media/channels/${id}">${channel.title}</a> by <a href="${member.url}">${member.handle} (${member.id})</a></b>`
  48. );
  49. }
  50. sendMessage(messages.join("\r\n\r\n"));
  51. return current;
  52. };
  53. // announce council change
  54. export const council = async (
  55. api: Api,
  56. council: Council,
  57. currentBlock: number,
  58. sendMessage: (msg: string) => void
  59. ): Promise<Council> => {
  60. const round: number = await api.query.councilElection.round();
  61. const stage: any = await api.query.councilElection.stage();
  62. const stageObj = JSON.parse(JSON.stringify(stage));
  63. let stageString = stageObj ? Object.keys(stageObj)[0] : "";
  64. let msg = "";
  65. if (!stage || stage.toJSON() === null) {
  66. stageString = "elected";
  67. const councilEnd: BlockNumber = await api.query.council.termEndsAt();
  68. const termDuration: BlockNumber = await api.query.councilElection.newTermDuration();
  69. const block = councilEnd.toNumber() - termDuration.toNumber();
  70. if (currentBlock - block < 2000) {
  71. const remainingBlocks = councilEnd.toNumber() - currentBlock;
  72. const m = moment().add(remainingBlocks * 6, "s");
  73. const endDate = formatTime(m, "DD/MM/YYYY");
  74. const handles: string[] = await Promise.all(
  75. (await api.query.council.activeCouncil()).map(
  76. async (seat: { member: string }) =>
  77. await memberHandleByAccount(api, seat.member)
  78. )
  79. );
  80. const members = handles.join(", ");
  81. msg = `Council election ended: ${members} have been elected for <a href="${domain}/#/council/members">council ${round}</a>. Congratulations!\nNext election starts on ${endDate}.`;
  82. }
  83. } else {
  84. const remainingBlocks = stage.toJSON()[stageString] - currentBlock;
  85. const m = moment().add(remainingBlocks * 6, "second");
  86. const endDate = formatTime(m, "DD-MM-YYYY HH:mm (UTC)");
  87. if (stageString === "Announcing")
  88. msg = `Council election started. You can <b><a href="${domain}/#/council/applicants">announce your application</a></b> until ${endDate}`;
  89. else if (stageString === "Voting")
  90. msg = `Council election: <b><a href="${domain}/#/council/applicants">Vote</a></b> until ${endDate}`;
  91. else if (stageString === "Revealing")
  92. msg = `Council election: <b><a href="${domain}/#/council/votes">Reveal your votes</a></b> until ${endDate}`;
  93. }
  94. if (round !== council.round && stageString !== council.last) sendMessage(msg);
  95. return { round, last: stageString };
  96. };
  97. // forum
  98. // announce latest categories
  99. export const categories = async (
  100. api: Api,
  101. category: number[],
  102. sendMessage: (msg: string) => void
  103. ): Promise<number> => {
  104. if (category[0] === category[1]) return category[0];
  105. const messages: string[] = [];
  106. for (let id: number = +category[0] + 1; id <= category[1]; id++) {
  107. const cat: Category = await query("title", () => categoryById(api, id));
  108. const msg = `Category ${id}: <b><a href="${domain}/#/forum/categories/${id}">${cat.title}</a></b>`;
  109. messages.push(msg);
  110. }
  111. sendMessage(messages.join("\r\n\r\n"));
  112. return category[1];
  113. };
  114. // announce latest posts
  115. export const posts = async (
  116. api: Api,
  117. posts: number[],
  118. sendMessage: (msg: string) => void
  119. ): Promise<number> => {
  120. const [last, current] = posts;
  121. if (current === last) return last;
  122. const messages: string[] = [];
  123. for (let id: number = +last + 1; id <= current; id++) {
  124. const post: Post = await query("current_text", () =>
  125. api.query.forum.postById(id)
  126. );
  127. const replyId: number = post.nr_in_thread.toNumber();
  128. const message: string = post.current_text;
  129. const excerpt: string = message.substring(0, 100);
  130. const threadId: number = post.thread_id.toNumber();
  131. const thread: Thread = await query("title", () =>
  132. api.query.forum.threadById(threadId)
  133. );
  134. const threadTitle: string = thread.title;
  135. const category: Category = await query("title", () =>
  136. categoryById(api, thread.category_id.toNumber())
  137. );
  138. const handle = await memberHandleByAccount(api, post.author_id.toJSON());
  139. 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>`;
  140. messages.push(msg);
  141. }
  142. sendMessage(messages.join("\r\n\r\n"));
  143. return current;
  144. };
  145. // announce latest proposals
  146. export const proposals = async (
  147. api: Api,
  148. prop: Proposals,
  149. block: number,
  150. sendMessage: (msg: string) => void
  151. ): Promise<Proposals> => {
  152. let { current, last, active, executing } = prop;
  153. for (let id: number = +last + 1; id <= current; id++) {
  154. const proposal: ProposalDetail = await proposalDetail(api, id);
  155. const { createdAt, finalizedAt, message, parameters, result } = proposal;
  156. const votingEndsAt = createdAt + parameters.votingPeriod.toNumber();
  157. const msg = `Proposal ${id} <b>created</b> at block ${createdAt}.\r\n${message}\r\nYou can vote until block ${votingEndsAt}.`;
  158. sendMessage(msg);
  159. active.push(id);
  160. }
  161. for (const id of active) {
  162. const proposal: ProposalDetail = await proposalDetail(api, id);
  163. const { finalizedAt, message, parameters, result, stage } = proposal;
  164. if (stage === "Finalized") {
  165. let label: string = result.toLowerCase();
  166. if (result === "Approved") {
  167. const executed = parameters.gracePeriod.toNumber() > 0 ? false : true;
  168. label = executed ? "executed" : "finalized";
  169. if (!executed) executing.push(id);
  170. }
  171. const msg = `Proposal ${id} <b>${label}</b> at block ${finalizedAt}.\r\n${message}`;
  172. sendMessage(msg);
  173. active = active.filter((a) => a !== id);
  174. }
  175. }
  176. for (const id of executing) {
  177. const proposal = await proposalDetail(api, id);
  178. const { exec, finalizedAt, message, parameters } = proposal;
  179. const executesAt = +finalizedAt + parameters.gracePeriod.toNumber();
  180. if (block < executesAt) continue;
  181. const execStatus = exec ? Object.keys(exec)[0] : "";
  182. const label = execStatus === "Executed" ? "has been" : "failed to be";
  183. const msg = `Proposal ${id} <b>${label} executed</b> at block ${executesAt}.\r\n${message}`;
  184. sendMessage(msg);
  185. executing = executing.filter((e) => e !== id);
  186. }
  187. return { current, last: current, active, executing };
  188. };
  189. // heartbeat
  190. const getAverage = (array: number[]): number =>
  191. array.reduce((a: number, b: number) => a + b, 0) / array.length;
  192. export const heartbeat = (
  193. api: Api,
  194. blocks: Block[],
  195. timePassed: string,
  196. proposals: Proposals,
  197. sendMessage: (msg: string) => void
  198. ): [] => {
  199. const durations = blocks.map((b) => b.duration);
  200. const blocktime = getAverage(durations) / 1000;
  201. const stake = blocks.map((b) => b.stake);
  202. const avgStake = getAverage(stake) / 1000000;
  203. const issued = blocks.map((b) => b.issued);
  204. const avgIssued = getAverage(issued) / 1000000;
  205. const percent = ((100 * avgStake) / avgIssued).toFixed(2);
  206. const noms = blocks.map((b) => b.noms);
  207. const vals = blocks.map((b) => b.vals);
  208. const avgVals = getAverage(vals);
  209. const totalReward = blocks.map((b) => b.reward);
  210. const avgReward = getAverage(totalReward);
  211. const reward = (avgReward / avgVals).toFixed();
  212. const active = proposals.active.length;
  213. const executing = proposals.executing.length;
  214. const p = (n: number) => (n > 1 ? "proposals" : "proposal");
  215. let props = active
  216. ? `\n<a href="${domain}/#/proposals">${active} active ${p(active)}</a> `
  217. : "";
  218. if (executing) props += `${executing} ${p(executing)} to be executed.`;
  219. sendMessage(
  220. ` ${blocks.length} blocks produced in ${timePassed}
  221. Blocktime: ${blocktime.toFixed(3)}s
  222. Stake: ${avgStake.toFixed(1)} / ${avgIssued.toFixed()} M tJOY (${percent}%)
  223. Validators: ${avgVals.toFixed()} (${reward} tJOY/h)
  224. Nominators: ${getAverage(noms).toFixed()}` + props
  225. );
  226. return [];
  227. };
  228. export const formatProposalMessage = (data: string[]): string => {
  229. const [id, title, type, stage, result, handle] = data;
  230. 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}`;
  231. };