bot.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import { Client } from "discord.js";
  2. import TelegramBot from "node-telegram-bot-api";
  3. import {
  4. discordToken,
  5. tgToken,
  6. chatid,
  7. heartbeat,
  8. proposalDelay,
  9. wsLocation,
  10. } from "../config";
  11. // types
  12. import { Block, Council, Options, Proposals } from "./types";
  13. import { types } from "@joystream/types";
  14. import { ApiPromise, WsProvider } from "@polkadot/api";
  15. import { AccountId, Header } from "@polkadot/types/interfaces";
  16. // functions
  17. import * as announce from "./lib/announcements";
  18. import * as get from "./lib/getters";
  19. import { parseArgs, printStatus, passedTime } from "./lib/util";
  20. import moment from "moment";
  21. const opts: Options = parseArgs(process.argv.slice(2));
  22. const log = (msg: string): void | number => opts.verbose && console.log(msg);
  23. process.env.NTBA_FIX_319 ||
  24. log("TL;DR: Set NTBA_FIX_319 to hide this warning.");
  25. // connect to telegram
  26. const bot = tgToken ? new TelegramBot(tgToken, { polling: true }) : null;
  27. // connect to discord
  28. let discordChannels: { [key: string]: any } = {};
  29. const client = new Client();
  30. client.login(discordToken);
  31. client.on("ready", async () => {
  32. if (!client.user) return;
  33. console.log(`Logged in to discord as ${client.user.tag}!`);
  34. discordChannels.council = await findDiscordChannel("council");
  35. discordChannels.proposals = await findDiscordChannel("proposals-bot");
  36. discordChannels.forum = await findDiscordChannel("forum-bot");
  37. discordChannels.tokenomics = await findDiscordChannel("tokenomics");
  38. });
  39. const findDiscordChannel = (name: string) =>
  40. client.channels.cache.find((channel: any) => channel.name === name);
  41. client.on("message", async (msg) => {
  42. const user = msg.author;
  43. if (msg.content === "/status") {
  44. msg.reply(`reporting to discord`);
  45. }
  46. });
  47. // send to telegram and discord
  48. const sendMessage = (msg: { tg: string; discord: string }, channel: any) => {
  49. if (msg.tg === "") return;
  50. sendDiscord(msg.discord, channel);
  51. sendTelegram(msg.tg);
  52. };
  53. const sendTelegram = (msg: string) => {
  54. try {
  55. if (bot) bot.sendMessage(chatid, msg, { parse_mode: "HTML" });
  56. else console.log(msg);
  57. } catch (e) {
  58. console.log(`Failed to send to telegram: ${e}`);
  59. }
  60. };
  61. const sendDiscord = (msg: string, channel: any) => {
  62. if (!channel || !msg.length) return;
  63. try {
  64. channel.send(msg);
  65. } catch (e) {
  66. console.log(e);
  67. }
  68. };
  69. const main = async () => {
  70. const provider = new WsProvider(wsLocation);
  71. const api = await ApiPromise.create({ provider, types });
  72. await api.isReady;
  73. const [chain, node, version] = await Promise.all([
  74. String(await api.rpc.system.chain()),
  75. api.rpc.system.name(),
  76. api.rpc.system.version(),
  77. ]);
  78. log(`Subscribed to ${chain} on ${node} v${version}`);
  79. let council: Council = { round: 0, last: "" };
  80. let blocks: Block[] = [];
  81. let lastEra = 0;
  82. let timestamp = await get.timestamp(api);
  83. let duration = 0;
  84. let lastHeartbeat = timestamp;
  85. let lastBlock: Block = {
  86. id: 0,
  87. duration: 0,
  88. timestamp: 0,
  89. stake: 0,
  90. noms: 0,
  91. vals: 0,
  92. issued: 0,
  93. reward: 0,
  94. };
  95. let issued = 0;
  96. let reward = 0;
  97. let stake = 0;
  98. let vals = 0;
  99. let noms = 0;
  100. let announced: { [key: string]: boolean } = {};
  101. const channels: number[] = [0, 0];
  102. const posts: number[] = [0, 0];
  103. const threads: number[] = [0, 0];
  104. let proposals: Proposals = { last: 0, current: 0, active: [], executing: [] };
  105. let lastProposalUpdate = 0;
  106. if (opts.forum) {
  107. posts[0] = await get.currentPostId(api);
  108. threads[0] = await get.currentThreadId(api);
  109. }
  110. if (opts.proposals) {
  111. proposals.last = await get.proposalCount(api);
  112. proposals.active = await get.activeProposals(api, proposals.last);
  113. }
  114. const getReward = async (era: number) =>
  115. Number(await api.query.staking.erasValidatorReward(era));
  116. api.rpc.chain.subscribeNewHeads(
  117. async (header: Header): Promise<void> => {
  118. // current block
  119. const id = header.number.toNumber();
  120. if (lastBlock.id === id) return;
  121. timestamp = await get.timestamp(api);
  122. duration = lastBlock.timestamp ? timestamp - lastBlock.timestamp : 0;
  123. // update validators and nominators every era
  124. const era = Number(await api.query.staking.currentEra());
  125. if (era > lastEra) {
  126. vals = (await api.query.session.validators()).length;
  127. stake = Number(await api.query.staking.erasTotalStake(era));
  128. issued = Number(await api.query.balances.totalIssuance());
  129. reward = (await getReward(era - 1)) || (await getReward(era - 2));
  130. // nominator count
  131. noms = 0;
  132. const nominators: { [key: string]: number } = {};
  133. const stashes = (await api.derive.staking.stashes())
  134. .map((s) => String(s))
  135. .map(async (v) => {
  136. const stakers = await api.query.staking.erasStakers(era, v);
  137. stakers.others.forEach(
  138. (n: { who: AccountId }) => nominators[String(n.who)]++
  139. );
  140. noms = Object.keys(nominators).length;
  141. });
  142. lastEra = era;
  143. }
  144. const block: Block = {
  145. id,
  146. timestamp,
  147. duration,
  148. stake,
  149. noms,
  150. vals,
  151. reward,
  152. issued,
  153. };
  154. if (duration) blocks = blocks.concat(block);
  155. // heartbeat
  156. if (timestamp > lastHeartbeat + heartbeat) {
  157. const time = passedTime(lastHeartbeat, timestamp);
  158. announce.heartbeat(
  159. api,
  160. blocks,
  161. time,
  162. proposals,
  163. sendMessage,
  164. discordChannels.tokenomics
  165. );
  166. lastHeartbeat = block.timestamp;
  167. blocks = [];
  168. }
  169. // announcements
  170. if (opts.council && block.id > lastBlock.id)
  171. council = await announce.council(
  172. api,
  173. council,
  174. block.id,
  175. sendMessage,
  176. discordChannels.council
  177. );
  178. if (opts.proposals) {
  179. proposals.current = await get.proposalCount(api);
  180. if (
  181. proposals.current > proposals.last &&
  182. !announced[proposals.current]
  183. ) {
  184. announced[`proposal${proposals.current}`] = true;
  185. proposals = await announce.proposals(
  186. api,
  187. proposals,
  188. id,
  189. sendMessage,
  190. discordChannels.proposals
  191. );
  192. lastProposalUpdate = timestamp;
  193. }
  194. }
  195. if (opts.forum) {
  196. posts[1] = await get.currentPostId(api);
  197. announce.posts(api, posts, sendMessage, discordChannels.forum);
  198. posts[0] = posts[1];
  199. }
  200. printStatus(opts, { block: id, chain, posts, proposals });
  201. lastBlock = block;
  202. }
  203. );
  204. };
  205. main().catch((error) => {
  206. console.log(error);
  207. process.exit();
  208. });