Просмотр исходного кода

Merge pull request #36 from traumschule/master

migrate bot to typescript
Martin 4 лет назад
Родитель
Сommit
fdf6c53f0a

+ 3 - 1
.gitignore

@@ -1 +1,3 @@
-.DS_Store
+.DS_Store
+node_modules/
+dist/

+ 51 - 22
community-contributions/joystreamtelegrambot/README.md

@@ -1,45 +1,74 @@
 # Joystream Forum Post Notification Tool
 
-Many thanks to [imOnlineMonitor](https://github.com/fkbenjamin/imOnlineMonitor) for providing example with polkadot chain (Kusama).\
-This script will notify several events on Joystream chain to Telegram group/channel/chat of your choice.\
-Current demo is https://t.me/jsforumnotification
+This bot notifies a Telegram chat about events on the Joystream chain.
 
-## Getting Started
-### Requirements
+Demo: https://t.me/jsforumnotification
+
+Many thanks to [imOnlineMonitor](https://github.com/fkbenjamin/imOnlineMonitor) for providing example with polkadot chain (Kusama).
+
+## Installation
 
 [Joystream Node](https://github.com/Joystream/helpdesk/tree/master/roles/validators#instructions)\
-[Yarn and Nodejs](https://github.com/Joystream/helpdesk/tree/master/roles/storage-providers#install-yarn-and-node-on-linux)
+[npm/Nodejs](https://github.com/Joystream/helpdesk/tree/master/roles/storage-providers#install-yarn-and-node-on-linux)
+
+```
+git clone https://github.com/bitoven-dev/joystreamtelegrambot
+cd joystreamtelegrambot
+npm install
+```
+
+## Configuration
+
+Open `config.ts` and set `token` and `chatid`. To get a bot token talk to @botfather on Telegram.
+
+Run `npm run build` to apply changes. After building \*.js files are available in `dist/` and you can run for example `node dist/bot.js --verbose --channel --council --forum --proposals`. For other options see below.
 
-### Run
-   ```
-   git clone https://github.com/bitoven-dev/joystreamtelegrambot
-   cd joystreamtelegrambot
-   yarn install
-   ```
-Replace `yourowntoken` on `const token = 'yourtoken';` with your Telgram bot token. You can get it by talking to @botfather 
+### Get chatid
 
-Replace `yourownchat` on `const chatid = 'yourchatid';` with your group/channel the bot will notify into. Bot needs to be added as admin, but it only needs to post message permission. [How to get chatid](https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id)
+Full explanation: [How to get chatid](https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id)
 
 ```
 1- Add the bot to the group.
 Go to the group, click on group name, click on Add members, in the searchbox search for your bot like this: @my_bot, select your bot and click add.
 
-2- Send a dummy message to the bot.
-You can use this example: /my_id @my_bot
-(I tried a few messages, not all the messages work. The example above works fine. Maybe the message should start with /)
+2- Send a message to the group.
 
-3- Go to following url: https://api.telegram.org/botXXX:YYYY/getUpdates
-replace XXX:YYYY with your bot token
+3- Go to `https://api.telegram.org/botXXX:YYYY/getUpdates` (replace XXX:YYYY with your bot token)
 
 4- Look for "chat":{"id":-zzzzzzzzzz,
--zzzzzzzzzz is your chat id (with the negative sign). 
+-zzzzzzzzzz is your chat id (including the negative sign).
 ```
 
-Run `node yourchoiceofscript.js` preferably inside screen/tmux window or systemd
+Run `npm run start` (for example inside screen/tmux window or systemd).
+
+## Running
+
+| Command                    | Description                                        |
+| :------------------------- | :------------------------------------------------- |
+| `npm run start`            | run without parameters.                            |
+| `npm run all`              | like `--channel --council --forum --proposals`     |
+| `npm run verbose`          | like 'all', log information on every block         |
+| `npm run quiet`            | like 'all' without output                          |
+| `npm run channel`          | only channel info                                  |
+| `npm run forum`            | only forum info                                    |
+| `npm run council`          | only council info                                  |
+| `npm run proposals`        | only proposal info                                 |
+| `npm run [module]-verbose` | run [module] and log every block                   |
+| `npm run dev`              | run all modules verbosely, respawn on file changes |
+| `npm run [module]-dev`     | run [module] verbosely, respawn on file changes    |
+
+If you need other combinations, add them to `package.json`
+
+## Development
+
+To test api queries open `https://testnet.joystream.org/` -> Settings -> change interface operation mode to `Fully featured` and got to `https://testnet.joystream.org/#/chainstate`.
 
 ## License
+
 [GPLv3](https://github.com/bitoven-dev/joystreamtelegrambot/blob/master/LICENSE)
 
 ### Notes
 
-I've just started to learn programming (JS), so any suggestions or PR is greatly appreciated 😁
+- bitoven created the first version written in javascript.
+
+- traumschule migrated and refactored it to typescript.

+ 0 - 381
community-contributions/joystreamtelegrambot/allbots.js

@@ -1,381 +0,0 @@
-const { registerJoystreamTypes } = require('@joystream/types');
-const { ApiPromise, WsProvider } = require('@polkadot/api');
-const TelegramBot = require('node-telegram-bot-api');
-// replace yourowntoken below with the Telegram token you receive from @BotFather
-const token = 'yourowntoken';
-// Create a bot that uses 'polling' to fetch new updates
-const bot = new TelegramBot(token);
-//get chat id here https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id
-const chatid = 'yourownchat';
-
-async function main () {
-	// register types before creating the api
-	registerJoystreamTypes()
-	// Create the API and wait until ready
-	const api = await ApiPromise.create({
-	  provider: new WsProvider() 
-	})
-
-	//proposals
-	let proposalcount = (await api.query.proposalsEngine.proposalCount()).toNumber()   
-	let activeproposals =  await getactiveProposals(api)
-	let filteredproposal
-	let tobeexecutedprop = await getpendingProposals(api)
-	let tobeexecutedpropfiltered
-
-	//forum
-	let lastpostnotif = await getcurrentPostId(api)
-	let lastcatnotif = await getcurrentCatId(api)
-	let lastthreadnotif = await getcurrentThreadId(api)
-
-	//channel
-	let lastchannelnotif = await getcurrentChannelId(api)
-
-	//council
-	var lastcouncilnotif = 0
-
-	const unsubscribe = await api.rpc.chain.subscribeNewHeads(async (header) => {
-		const block = header.number.toNumber()
-		
-		//proposals
-		const currentproposal = (await api.query.proposalsEngine.proposalCount()).toNumber()
-		console.log(`Current block: ${block}, Current proposal count: ${currentproposal}, Current active proposal : ${activeproposals}`)
-		if (currentproposal>proposalcount) {
-            for (proposalcount+1;proposalcount<currentproposal;proposalcount++) {
-                const proposal = await getproposalDetail(api,proposalcount+1)
-                const propcreatedtime = proposal.detail().createdAt.toJSON()
-
-                console.log(`New proposal (${proposalcount+1}) created at block ${propcreatedtime}.\r\n ${proposal.postmessage()}`)
-                bot.sendMessage(chatid, `New proposal (${proposalcount+1}) created at block ${propcreatedtime}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                activeproposals.push(proposalcount+1)
-            }            
-        }
-
-        if (activeproposals[0]>0) {
-            for (const proposallist of activeproposals){
-                const proposal = await getproposalDetail(api,proposallist)
-                let propstage = proposal.stage()[0]
-                if (propstage == 'Finalized') {
-                    const propstatus = proposal.resultjson()
-                    switch (propstatus[0]){
-                        case 'Approved':
-                            let graceperiod = proposal.graceperiod()
-                            if (graceperiod>0) {
-                                bot.sendMessage(chatid, `Proposalid (${proposallist}) status changed to "Finalized" at block ${proposal.finalizedtime()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                                filteredproposal = activeproposals.filter(e => e != proposallist)
-                                tobeexecutedprop.push(proposallist)
-                            } else {
-                                bot.sendMessage(chatid, `Proposalid (${proposallist}) status changed to "Finalized and Executed" at block ${proposal.finalizedtime()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                                filteredproposal = activeproposals.filter(e => e != proposallist)
-                            }
-                            break;
-                        case 'Expired':
-                        case 'Canceled':
-                        case 'Cancelled':
-                        case 'Rejected':
-                        case 'Slashed':
-                        case 'Vetoed':
-                            // console.log(`Proposal ${proposallist} ${propstatus[0]}`)
-                            // bot.sendMessage(chatid, `Proposalid (${proposallist}) status changed to <b>"Finalized:${propstatus[0]}"</b> at block ${proposal.finalizedtime()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                            // filteredproposal = activeproposals.filter(e => e != proposallist)
-                            // break;
-                        default:
-                            console.log(`Proposal ${proposallist} changed to other status: ${propstatus[0]}`)
-                            bot.sendMessage(chatid, `Proposalid (${proposallist}) status changed to <b>"Finalized:${propstatus[0]}"</b> at block ${proposal.finalizedtime()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                            filteredproposal = activeproposals.filter(e => e != proposallist)
-                            break;
-                    }
-                    activeproposals = filteredproposal
-                }
-            } 
-        }
-        if (tobeexecutedprop[0]>0) {
-            for (const proposallist of tobeexecutedprop) {
-                const proposal = await getproposalDetail(api,proposallist)
-                let exestatus = Object.getOwnPropertyNames(proposal.resultfull()['Approved'])[0]
-                if (exestatus=='Executed'){
-                    console.log(`Proposal ${proposallist} has been executed`)
-                    bot.sendMessage(chatid, `Proposalid (${proposallist}) <b>has been executed</b> at block ${proposal.finalizedtime()+proposal.graceperiod()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                    tobeexecutedpropfiltered = tobeexecutedprop.filter(e => e != proposallist)
-                } else {
-                    console.log(`Proposal ${proposallist} Execution is failed`)
-                    bot.sendMessage(chatid, `Proposalid (${proposallist}) <b>failed to be executed</b> at block ${proposal.finalizedtime()+proposal.graceperiod()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                    tobeexecutedpropfiltered = tobeexecutedprop.filter(e => e != proposallist)
-                }    
-                tobeexecutedprop = tobeexecutedpropfiltered
-            }
-		}
-		//forum
-		const currentpost = await getcurrentPostId(api)
-		const currentcat = await getcurrentCatId(api)
-		const currentthread = await getcurrentThreadId(api)
-	
-		console.log(`Current block: ${block}, Latest postid: ${currentpost}, Latest categoryid: ${currentcat}, Latest threadid:${currentthread}`)
-		
-		//category forum checking
-		if (currentcat>lastcatnotif){
-			for (lastcatnotif+1; lastcatnotif<currentcat; lastcatnotif++){
-				const categorytitle = await getcategoryTitle(api,lastcatnotif+1)
-				console.log('Notify category',lastcatnotif+1, 'to Telegram')
-				bot.sendMessage(chatid, `New category created: <b><a href="https://testnet.joystream.org/#/forum/categories/${lastcatnotif+1}">${categorytitle}</a>, Category ID: ${lastcatnotif+1}</b>`, { parse_mode: 'html' })
-			}
-
-		}
-		
-		//thread forum checking
-		if (currentthread>lastthreadnotif){
-			const newthread = []
-			for (lastthreadnotif+1; lastthreadnotif<currentthread; lastthreadnotif++){
-				const threadid = await api.query.forum.threadById(lastthreadnotif+1)
-				const threadtitle = threadid.title
-				const currentcategory = threadid.category_id
-				const categorytitle = await getcategoryTitle(api,currentcategory)
-				const authoraddress = threadid.author_id.toJSON()
-				const memberraw = await api.query.members.memberIdsByRootAccountId(authoraddress)
-				const memberid = memberraw[0].toNumber()
-				const handler = await getmemberHandle(api, memberid)
-				console.log('Notify thread',lastthreadnotif+1, 'to Telegram')
-				//sent to array			
-				newthread.push(`New thread created: <a href="https://testnet.joystream.org/#/forum/threads/${lastthreadnotif+1}">"${threadtitle}"</a> by <a href="https://testnet.joystream.org/#/members/${handler}">${handler}</a> (id:${memberid}) in category "<a href="https://testnet.joystream.org/#/forum/categories/${currentcategory}">${categorytitle}</a>" `)
-			}
-		bot.sendMessage(chatid, newthread.join("\r\n\r\n"), { parse_mode: 'html' })
-		}
-			
-		//forum post checking
-    	if (currentpost>lastpostnotif) {
-			console.log(currentpost-lastpostnotif, ' new posts')
-			const newpost = []
-			//loop notification for every new post published since lastnotif
-			for (lastpostnotif+1; lastpostnotif<currentpost; lastpostnotif++) {
-				//begin chaining query info
-				const postbyid = await api.query.forum.postById(lastpostnotif+1)
-				const postpos = postbyid.nr_in_thread
-				const message = postbyid.current_text
-				//limit characters for message on telegram
-				const excerpt = message.substring(0,100)
-				const currentthreadid = postbyid.thread_id.toNumber()
-				const threadid = await api.query.forum.threadById(currentthreadid)
-				const threadtitle = threadid.title
-				const currentcategory = threadid.category_id
-				const categorytitle = await getcategoryTitle(api,currentcategory)
-				const authoraddress = postbyid.author_id.toJSON()
-				const memberraw = await api.query.members.memberIdsByRootAccountId(authoraddress)
-				const memberid = memberraw[0].toNumber()
-				const handler = await getmemberHandle(api, memberid)
-				console.log('Notify post',lastpostnotif+1, 'to Telegram')
-				//sent to array			
-				newpost.push(`🤩<b> New post (id:${lastpostnotif+1}) by <a href="https://testnet.joystream.org/#/members/${handler}">${handler}</a> (id:${memberid}) in category "<a href="https://testnet.joystream.org/#/forum/categories/${currentcategory}">${categorytitle}</a>" at:\r\n<a href="https://testnet.joystream.org/#/forum/threads/${currentthreadid}?replyIdx=${postpos}">"${threadtitle}"</a></b><i>\r\n"${excerpt}..."</i>\r\n`)
-			}
-		//console.log(newpost.join("\r\n\r\n"))
-		bot.sendMessage(chatid, newpost.join("\r\n\r\n"), { parse_mode: 'html' })
-		//update lastnotif
-		// lastpostnotif=currentpost
-		}
-
-
-		//channel
-		const currentchannelid = await getcurrentChannelId(api)
-        console.log('Latest channelid is :',currentchannelid)
-
-        if (currentchannelid>lastchannelnotif) {
-            const newchannel = []
-            for (lastchannelnotif+1;lastchannelnotif<currentchannelid;lastchannelnotif++) {
-                const channel = await (await api.query.contentWorkingGroup.channelById(lastchannelnotif+1)).toJSON()
-                const channeltitle = channel[0].title
-                const memberid = channel[0].owner
-                const channelownerhandler = await getmemberHandle(api, memberid)
-                console.log(`Posting channel id: ${lastchannelnotif+1} to Telegram`)
-                newchannel.push(`<b>New channel id created:</b>${lastchannelnotif+1}\r\n<b>Channel Title:</b><a href="https://testnet.joystream.org/#/media/channels/${lastchannelnotif+1}"> ${channeltitle}</a>\r\n<b>Member ID:</b> ${memberid}\r\n<b>Member Handler:</b> <a href="https://testnet.joystream.org/#/members/${channelownerhandler}">${channelownerhandler}</a>`)
-            }
-            bot.sendMessage(chatid, newchannel.join("\r\n\r\n"), { parse_mode: 'html' })
-		}
-		
-		//council
-
-		if (block>lastcouncilnotif) {
-            const councilround = await api.query.councilElection.round()
-            const councilendterm = (await api.query.council.termEndsAt()).toNumber()
-            const annperiod = (await api.query.councilElection.announcingPeriod()).toNumber()
-            const votingperiod = (await api.query.councilElection.votingPeriod()).toNumber()
-            const revealingperiod = (await api.query.councilElection.revealingPeriod()).toNumber()
-            const councilstage = await getcouncilStage(api)
-            const councilperiod = (await api.query.councilElection.newTermDuration()).toNumber() 
-            switch (councilstage){
-                case null:
-                    console.log('Council has been elected')
-                    if (block>lastcouncilnotif){
-                        bot.sendMessage(chatid, `<a href="https://testnet.joystream.org/#/council/members"> New council for round ${councilround}</a> has been elected at block ${councilendterm-councilperiod}.`, { parse_mode: 'html' })
-                        lastcouncilnotif=councilendterm
-                    }
-                    break;
-
-                default:
-                    const annstage = councilstage.Announcing
-                    const votingstage = councilstage.Voting
-                    const revealingstage = councilstage.Revealing
-                    if (annstage) {
-                        console.log('Announcing Stage')
-                        if (block>lastcouncilnotif){
-                            bot.sendMessage(chatid, `New council election for round ${councilround} has been started at block ${annstage-annperiod}.<a href="https://testnet.joystream.org/#/council/applicants"> You can apply now!</a>`, { parse_mode: 'html' })
-                            lastcouncilnotif=annstage
-                        }
-                    }
-                    if (votingstage) {
-                        console.log('Voting Stage')
-                        if (block>lastcouncilnotif){
-                            bot.sendMessage(chatid, `Voting stage for council election has been started at block ${votingstage-votingperiod}. <a href="https://testnet.joystream.org/#/council/applicants">You can vote now!</a>`, { parse_mode: 'html' })
-                            lastcouncilnotif=votingstage
-                        }
-                    }
-                    if (revealingstage) {
-                        console.log('Revealing Stage')
-                        if (block>lastcouncilnotif){
-                            bot.sendMessage(chatid, `Revealing stage for council election has been started at block ${revealingstage-revealingperiod}. <a href="https://testnet.joystream.org/#/council/votes">Don't forget to reveal your vote!</a>`, { parse_mode: 'html' })
-                            lastcouncilnotif=revealingstage
-                        }
-                    }       
-
-                    break;
-            }
-        }
-		
-
-
-	})
-}
-
-//functions
-
-//proposals
-const getpendingProposals = async (api) => {
-    let tobeexecutedprop = ((await api.query.proposalsEngine.pendingExecutionProposalIds()).toJSON())[0]
-    if (tobeexecutedprop[0]==0){
-        return []
-    } else {
-        return tobeexecutedprop
-    }
-}
-
-const getactiveProposals = async (api) => {
-    let activeproposals =  ((await api.query.proposalsEngine.activeProposalIds()).toJSON())[0]
-    if (activeproposals[0]==0){
-        return []
-    } else {
-        return activeproposals
-    }
-}
-
-const getmemberHandle = async (api,memberid) => {
-    const memberprofile = await api.query.members.memberProfile(memberid)
-    const handler = memberprofile.raw.handle.toJSON()
-    return handler
-}
-
-const getproposalStatus = (propresultraw) => {
-    if (propresultraw.hasOwnProperty('proposalStatus')) {
-        return propresultraw.proposalStatus
-    } else {
-        return {Active:null}
-    }
-} 
-
-const getfinalTime = (propresultraw) => {
-    if (propresultraw.hasOwnProperty('finalizedAt')) {
-        return propresultraw.finalizedAt
-    } else {
-        return 0
-    }
-}
-
-const getproposalDetail = async (api,proposalcount) => {
-    const propdetail = await api.query.proposalsEngine.proposals(proposalcount)
-    const parameters = propdetail.parameters
-    const propposterid = propdetail.proposerId.toJSON()
-    const handler = await getmemberHandle(api,propposterid)
-    const proptype = await api.query.proposalsCodex.proposalDetailsByProposalId(proposalcount)
-    const [deftype] = Object.getOwnPropertyNames(proptype.toJSON())
-    const proptitle = propdetail.get("title")
-    const propstage = propdetail.status.toJSON()
-    // const propstatus = propdetail.get("status")
-    const propstatus = Object.getOwnPropertyNames(propstage)
-    const propresultraw =  propstage[propstatus]
-    const propfinalresultfull = getproposalStatus(propresultraw)
-    // const propfinalresultfull = propresultraw.proposalStatus
-    // const propfinalresultjson = Object.getOwnPropertyNames(propresultraw.proposalStatus)
-    const propfinaltime = getfinalTime(propresultraw)
-    // const propfinaltime = propresultraw.finalizedAt
-    const propfinalresultjson = Object.getOwnPropertyNames(propfinalresultfull)
-    const graceperiod = propdetail.parameters.gracePeriod.toNumber()
-    return {
-        detail : function () {
-            return propdetail;
-        },
-        parameters : function () {
-            return parameters;
-        },
-        stage : function () {
-            return propstatus;
-        },
-        finalizedtime : function () {
-            return propfinaltime;
-        },
-        graceperiod : function () {
-            return graceperiod;
-        },
-        resultfull : function () {
-            return propfinalresultfull;
-        },
-        resultjson : function () {
-            return propfinalresultjson;
-        },
-        postmessage : function () {
-            return `<b>Type</b>: ${deftype}\r\n <b>Proposer</b>:<a href="https://testnet.joystream.org/#/members/${handler}"> ${handler}(${propposterid})</a>\r\n <b>Title</b>: <a href="https://testnet.joystream.org/#/proposals/${proposalcount}">${proptitle}</a>\r\n <b>Stage</b>: ${propstatus}\r\n <b>Result</b>: ${JSON.stringify(propfinalresultfull)}`;
-        // postmessage : function () {
-        //     return `<b>Type</b>: ${this.deftype()}\r\n <b>Proposer</b>: ${this.handler()}(${this.posterid()})\r\n <b>Title</b>: ${this.title()}\r\n <b>Stage</b>: ${this.stage()}\r\n <b>Result</b>: ${this.result()}`;
-        }
-    }
-}
-//forum
-const getcategoryTitle = async (api, categoryid) => {
-    const category = await api.query.forum.categoryById(categoryid)
-	const categorytitle = category.title
-	return categorytitle
-}
-
-const getcurrentPostId = async (api) => {
-    const nextpostid = await api.query.forum.nextPostId()
-	const currentpostid = nextpostid.toNumber()-1
-	return currentpostid
-}
-
-const getcurrentThreadId = async (api) => {
-    const nextthreadid = await api.query.forum.nextThreadId()
-	const currentthreadid = nextthreadid.toNumber()-1
-	return currentthreadid
-}
-
-const getcurrentCatId = async (api) => {
-    const nextcatid = await api.query.forum.nextCategoryId()
-	const currentcatid = nextcatid.toNumber()-1
-	return currentcatid
-}
-
-
-
-//channel
-
-const getcurrentChannelId = async (api) => {
-    const nextchannelid = await api.query.contentWorkingGroup.nextChannelId()
-	const currentchannelid = nextchannelid.toNumber()-1
-	return currentchannelid
-}
-
-//council
-const getcouncilStage = async (api) => {
-    const councilstage = await api.query.councilElection.stage()
-	const councilstagejson = councilstage.toJSON()
-	return councilstagejson
-}
-
-
-main()

+ 0 - 52
community-contributions/joystreamtelegrambot/channel.js

@@ -1,52 +0,0 @@
-const { registerJoystreamTypes } = require('@joystream/types');
-const { ApiPromise, WsProvider } = require('@polkadot/api');
-const TelegramBot = require('node-telegram-bot-api');
-// replace the value below with the Telegram token you receive from @BotFather
-const token = 'yourowntoken';
-// Create a bot that uses 'polling' to fetch new updates
-const bot = new TelegramBot(token);
-//get chat id here https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id
-const chatid = 'yourownchat';
-
-async function main () {
-    registerJoystreamTypes()
-    // Create the API and wait until ready
-    const api = await ApiPromise.create({
-        provider: new WsProvider() 
-    })
-    let lastchannelnotif = await getcurrentChannelId(api)
-    //let lastchannelnotif = 25
-
-    const unsubscribe = await api.rpc.chain.subscribeNewHeads(async (header) => {
-        const currentchannelid = await getcurrentChannelId(api)
-        console.log('Latest channelid is :',currentchannelid)
-
-        if (currentchannelid>lastchannelnotif) {
-            const newchannel = []
-            for (lastchannelnotif+1;lastchannelnotif<currentchannelid;lastchannelnotif++) {
-                const channel = await (await api.query.contentWorkingGroup.channelById(lastchannelnotif+1)).toJSON()
-                const channeltitle = channel[0].title
-                const memberid = channel[0].owner
-                const channelownerhandler = await getmemberHandle(api, memberid)
-                console.log(`Posting channel id: ${lastchannelnotif+1} to Telegram`)
-                newchannel.push(`<b>New channel id created:</b>${lastchannelnotif+1}\r\n<b>Channel Title:</b><a href="https://testnet.joystream.org/#/media/channels/${lastchannelnotif+1}"> ${channeltitle}</a>\r\n<b>Member ID:</b> ${memberid}\r\n<b>Member Handler:</b> <a href="https://testnet.joystream.org/#/members/${channelownerhandler}">${channelownerhandler}</a>`)
-            }
-            bot.sendMessage(chatid, newchannel.join("\r\n\r\n"), { parse_mode: 'html' })
-        }
-    })
-}
-
-const getmemberHandle = async (api,memberid) => {
-    const memberprofile = await api.query.members.memberProfile(memberid)
-    const handler = memberprofile.raw.handle.toJSON()
-    return handler
-}
-
-const getcurrentChannelId = async (api) => {
-    const nextchannelid = await api.query.contentWorkingGroup.nextChannelId()
-	const currentchannelid = nextchannelid.toNumber()-1
-	return currentchannelid
-}
-
-
-main()

+ 11 - 0
community-contributions/joystreamtelegrambot/config.ts

@@ -0,0 +1,11 @@
+// website
+export const domain = "https://testnet.joystream.org";
+
+export const wsLocation = "ws://rome-rpc-endpoint.joystream.org:9944/";
+// websocket location
+
+// telegram bot token
+export const token: string = "";
+
+// telegram chat ID
+export const chatid: string = "";

+ 0 - 78
community-contributions/joystreamtelegrambot/council.js

@@ -1,78 +0,0 @@
-const { registerJoystreamTypes } = require('@joystream/types');
-const { ApiPromise, WsProvider } = require('@polkadot/api');
-const TelegramBot = require('node-telegram-bot-api');
-// replace the value below with the Telegram token you receive from @BotFather
-const token = 'yourowntoken';
-// Create a bot that uses 'polling' to fetch new updates
-const bot = new TelegramBot(token);
-//get chat id here https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id
-const chatid = 'yourownchat';
-
-async function main () {
-    registerJoystreamTypes()
-    // Create the API and wait until ready
-    const api = await ApiPromise.create({
-        provider: new WsProvider() 
-    })
-    var lastcouncilnotif = 0
-
-    const unsubscribe = await api.rpc.chain.subscribeNewHeads(async (header) => {
-        const block = header.number.toNumber()
-        
-        console.log(`Current block ${block}`)        
-        if (block>lastcouncilnotif) {
-            const councilround = await api.query.councilElection.round()
-            const councilendterm = (await api.query.council.termEndsAt()).toNumber()
-            const annperiod = (await api.query.councilElection.announcingPeriod()).toNumber()
-            const votingperiod = (await api.query.councilElection.votingPeriod()).toNumber()
-            const revealingperiod = (await api.query.councilElection.revealingPeriod()).toNumber()
-            const councilstage = await getcouncilStage(api)
-            const councilperiod = (await api.query.councilElection.newTermDuration()).toNumber() 
-            switch (councilstage){
-                case null:
-                    console.log('Council has been elected')
-                    if (block>lastcouncilnotif){
-                        bot.sendMessage(chatid, `<a href="https://testnet.joystream.org/#/council/members"> New council for round ${councilround}</a> has been elected at block ${councilendterm-councilperiod}.`, { parse_mode: 'html' })
-                        lastcouncilnotif=councilendterm
-                    }
-                    break;
-
-                default:
-                    const annstage = councilstage.Announcing
-                    const votingstage = councilstage.Voting
-                    const revealingstage = councilstage.Revealing
-                    if (annstage) {
-                        console.log('Announcing Stage')
-                        if (block>lastcouncilnotif){
-                            bot.sendMessage(chatid, `New council election for round ${councilround} has been started at block ${annstage-annperiod}.<a href="https://testnet.joystream.org/#/council/applicants"> You can apply now!</a>`, { parse_mode: 'html' })
-                            lastcouncilnotif=annstage
-                        }
-                    }
-                    if (votingstage) {
-                        console.log('Voting Stage')
-                        if (block>lastcouncilnotif){
-                            bot.sendMessage(chatid, `Voting stage for council election has been started at block ${votingstage-votingperiod}. <a href="https://testnet.joystream.org/#/council/applicants">You can vote now!</a>`, { parse_mode: 'html' })
-                            lastcouncilnotif=votingstage
-                        }
-                    }
-                    if (revealingstage) {
-                        console.log('Revealing Stage')
-                        if (block>lastcouncilnotif){
-                            bot.sendMessage(chatid, `Revealing stage for council election has been started at block ${revealingstage-revealingperiod}. <a href="https://testnet.joystream.org/#/council/votes">Don't forget to reveal your vote!</a>`, { parse_mode: 'html' })
-                            lastcouncilnotif=revealingstage
-                        }
-                    }       
-
-                    break;
-            }
-        }
-    })
-}
-
-const getcouncilStage = async (api) => {
-    const councilstage = await api.query.councilElection.stage()
-	const councilstagejson = councilstage.toJSON()
-	return councilstagejson
-}
-
-main()

+ 0 - 126
community-contributions/joystreamtelegrambot/forum.js

@@ -1,126 +0,0 @@
-const { registerJoystreamTypes } = require('@joystream/types');
-const { ApiPromise, WsProvider } = require('@polkadot/api');
-const TelegramBot = require('node-telegram-bot-api');
-// replace the value below with the Telegram token you receive from @BotFather
-const token = 'yourowntoken';
-// Create a bot that uses 'polling' to fetch new updates
-const bot = new TelegramBot(token);
-//get chat id here https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id
-const chatid = 'yourownchat';
-
-async function main () {
-  // register types before creating the api
-  registerJoystreamTypes()
-  // Create the API and wait until ready
-  const api = await ApiPromise.create({
-    provider: new WsProvider() 
-    })
-
-    //set (initial) lastpostnotification sent by bot, 
-	let lastpostnotif = await getcurrentPostId(api)
-	let lastcatnotif = await getcurrentCatId(api)
-	let lastthreadnotif = await getcurrentThreadId(api)
-
-    //subscribing to new heads of the chain
-    const unsubscribe = await api.rpc.chain.subscribeNewHeads(async (header) => {
-		const currentpost = await getcurrentPostId(api)
-		const currentcat = await getcurrentCatId(api)
-		const currentthread = await getcurrentThreadId(api)
-	
-    	const block = header.number.toNumber()
-		console.log(`Current block: ${block}, Latest postid: ${currentpost}, Latest categoryid: ${currentcat}, Latest threadid:${currentthread}`)
-		
-		//category forum checking
-		if (currentcat>lastcatnotif){
-			for (lastcatnotif+1; lastcatnotif<currentcat; lastcatnotif++){
-				const categorytitle = await getcategoryTitle(api,lastcatnotif+1)
-				console.log('Notify category',lastcatnotif+1, 'to Telegram')
-				bot.sendMessage(chatid, `New category created: <b><a href="https://testnet.joystream.org/#/forum/categories/${lastcatnotif+1}">${categorytitle}</a>, Category ID: ${lastcatnotif+1}</b>`, { parse_mode: 'html' })
-			}
-
-		}
-		
-		//thread forum checking
-		if (currentthread>lastthreadnotif){
-			const newthread = []
-			for (lastthreadnotif+1; lastthreadnotif<currentthread; lastthreadnotif++){
-				const threadid = await api.query.forum.threadById(lastthreadnotif+1)
-				const threadtitle = threadid.title
-				const currentcategory = threadid.category_id
-				const categorytitle = await getcategoryTitle(api,currentcategory)
-				const authoraddress = threadid.author_id.toJSON()
-				const memberraw = await api.query.members.memberIdsByRootAccountId(authoraddress)
-				const memberid = memberraw[0].toNumber()
-				const handler = await getmemberHandle(api, memberid)
-				console.log('Notify thread',lastthreadnotif+1, 'to Telegram')
-				//sent to array			
-				newthread.push(`New thread created: <a href="https://testnet.joystream.org/#/forum/threads/${lastthreadnotif+1}">"${threadtitle}"</a> by <a href="https://testnet.joystream.org/#/members/${handler}">${handler}</a> (id:${memberid}) in category "<a href="https://testnet.joystream.org/#/forum/categories/${currentcategory}">${categorytitle}</a>" `)
-			}
-		bot.sendMessage(chatid, newthread.join("\r\n\r\n"), { parse_mode: 'html' })
-		}
-			
-		//forum post checking
-    	if (currentpost>lastpostnotif) {
-			console.log(currentpost-lastpostnotif, ' new posts')
-			const newpost = []
-			//loop notification for every new post published since lastnotif
-			for (lastpostnotif+1; lastpostnotif<currentpost; lastpostnotif++) {
-				//begin chaining query info
-				const postbyid = await api.query.forum.postById(lastpostnotif+1)
-				const postpos = postbyid.nr_in_thread
-				const message = postbyid.current_text
-				//limit characters for message on telegram
-				const excerpt = message.substring(0,100)
-				const currentthreadid = postbyid.thread_id.toNumber()
-				const threadid = await api.query.forum.threadById(currentthreadid)
-				const threadtitle = threadid.title
-				const currentcategory = threadid.category_id
-				const categorytitle = await getcategoryTitle(api,currentcategory)
-				const authoraddress = postbyid.author_id.toJSON()
-				const memberraw = await api.query.members.memberIdsByRootAccountId(authoraddress)
-				const memberid = memberraw[0].toNumber()
-				const handler = await getmemberHandle(api, memberid)
-				console.log('Notify post',lastpostnotif+1, 'to Telegram')
-				//sent to array			
-				newpost.push(`🤩<b> New post (id:${lastpostnotif+1}) by <a href="https://testnet.joystream.org/#/members/${handler}">${handler}</a> (id:${memberid}) in category "<a href="https://testnet.joystream.org/#/forum/categories/${currentcategory}">${categorytitle}</a>" at:\r\n<a href="https://testnet.joystream.org/#/forum/threads/${currentthreadid}?replyIdx=${postpos}">"${threadtitle}"</a></b><i>\r\n"${excerpt}..."</i>\r\n`)
-			}
-		//console.log(newpost.join("\r\n\r\n"))
-		bot.sendMessage(chatid, newpost.join("\r\n\r\n"), { parse_mode: 'html' })
-		//update lastnotif
-		// lastpostnotif=currentpost
-		}
-		  
-	});
-}
-
-const getcategoryTitle = async (api, categoryid) => {
-    const category = await api.query.forum.categoryById(categoryid)
-	const categorytitle = category.title
-	return categorytitle
-}
-
-const getcurrentPostId = async (api) => {
-    const nextpostid = await api.query.forum.nextPostId()
-	const currentpostid = nextpostid.toNumber()-1
-	return currentpostid
-}
-
-const getcurrentThreadId = async (api) => {
-    const nextthreadid = await api.query.forum.nextThreadId()
-	const currentthreadid = nextthreadid.toNumber()-1
-	return currentthreadid
-}
-
-const getcurrentCatId = async (api) => {
-    const nextcatid = await api.query.forum.nextCategoryId()
-	const currentcatid = nextcatid.toNumber()-1
-	return currentcatid
-}
-
-const getmemberHandle = async (api,memberid) => {
-    const memberprofile = await api.query.members.memberProfile(memberid)
-    const handler = memberprofile.raw.handle.toJSON()
-    return handler
-}
-
-main()

+ 38 - 5
community-contributions/joystreamtelegrambot/package.json

@@ -1,7 +1,40 @@
 {
-   "dependencies": {
-      "@joystream/types": "^0.12.0",
-      "@polkadot/api": "^0.96.1",
-      "node-telegram-bot-api": "^0.50.0"
-   }
+  "repository": "https://github.com/Joystream/community-repo",
+  "license": "GPL-3.0-or-later",
+  "scripts": {
+    "build": "tsc",
+    "start": "ts-node src/bot.ts",
+    "all": "ts-node src/bot.ts --channel --council --forum --proposals",
+    "quiet": "ts-node src/bot.ts --quiet --channel --council --forum --proposals",
+    "verbose": "ts-node src/bot.ts --verbose --channel --council --forum --proposals",
+    "channel": "ts-node src/bot.ts --channel",
+    "council": "ts-node src/bot.ts --council",
+    "forum": "ts-node src/bot.ts --forum",
+    "proposals": "ts-node src/bot.ts --proposals",
+    "channel-verbose": "ts-node src/bot.ts --verbose --channel",
+    "council-verbose": "ts-node src/bot.ts --verbose --council",
+    "forum-verbose": "ts-node src/bot.ts --verbose --forum",
+    "proposals-verbose": "ts-node src/bot.ts --proposals",
+    "dev": "ts-node-dev --respawn src/bot.ts --verbose --channel --council --forum --proposals",
+    "channel-dev": "ts-node-dev --respawn src/bot.ts --verbose --channel",
+    "council-dev": "ts-node-dev --respawn src/bot.ts --verbose --council",
+    "forum-dev": "ts-node-dev --respawn src/bot.ts --verbose --forum",
+    "proposal-dev": "ts-node-dev --respawn src/bot.ts --verbose --proposals",
+    "tests": "ts-node src/tests.ts"
+  },
+  "dependencies": {
+    "@joystream/types": "^0.13.1",
+    "@polkadot/api": "1.26.1",
+    "@polkadot/keyring": "^3.0.1",
+    "@polkadot/util": "^3.0.1",
+    "@types/node-telegram-bot-api": "^0.50.0",
+    "moment": "^2.29.1",
+    "node-telegram-bot-api": "^0.50.0"
+  },
+  "devDependencies": {
+    "@polkadot/ts": "^0.3.49",
+    "ts-node": "^9.0.0",
+    "ts-node-dev": "^1.0.0-pre.63",
+    "typescript": "^4.0.3"
+  }
 }

+ 0 - 188
community-contributions/joystreamtelegrambot/proposal.js

@@ -1,188 +0,0 @@
-const { registerJoystreamTypes } = require('@joystream/types');
-const { ApiPromise, WsProvider } = require('@polkadot/api');
-const TelegramBot = require('node-telegram-bot-api');
-// replace yourowntoken below with the Telegram token you receive from @BotFather
-const token = 'yourowntoken';
-const bot = new TelegramBot(token);
-//replace yourownchat, get chat id here https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id
-const chatid = 'yourownchat';
-
-async function main () {
-    registerJoystreamTypes()
-    // Create the API and wait until ready
-    const api = await ApiPromise.create({
-        provider: new WsProvider() 
-    })
-    
-    let proposalcount = (await api.query.proposalsEngine.proposalCount()).toNumber()   
-    // let activeproposals = []
-    // let activeproposals =  ((await api.query.proposalsEngine.activeProposalIds()).toJSON())[0]
-    
-    let activeproposals =  await getactiveProposals(api)
-    let filteredproposal
-    let tobeexecutedprop = await getpendingProposals(api)
-    let tobeexecutedpropfiltered
-    
-
-    const unsubscribe = await api.rpc.chain.subscribeNewHeads(async (header) => {
-        const block = header.number.toNumber()
-        const currentproposal = (await api.query.proposalsEngine.proposalCount()).toNumber()
-        console.log(`Current block: ${block}, Current proposal count: ${currentproposal}, Current active proposal : ${activeproposals}`)
-        if (currentproposal>proposalcount) {
-            for (proposalcount+1;proposalcount<currentproposal;proposalcount++) {
-                const proposal = await getproposalDetail(api,proposalcount+1)
-                const propcreatedtime = proposal.detail().createdAt.toJSON()
-
-                console.log(`New proposal (${proposalcount+1}) created at block ${propcreatedtime}.\r\n ${proposal.postmessage()}`)
-                bot.sendMessage(chatid, `New proposal (${proposalcount+1}) created at block ${propcreatedtime}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                activeproposals.push(proposalcount+1)
-            }            
-        }
-
-        if (activeproposals[0]>0) {
-            for (const proposallist of activeproposals){
-                const proposal = await getproposalDetail(api,proposallist)
-                let propstage = proposal.stage()[0]
-                if (propstage == 'Finalized') {
-                    const propstatus = proposal.resultjson()
-                    switch (propstatus[0]){
-                        case 'Approved':
-                            let graceperiod = proposal.graceperiod()
-                            if (graceperiod>0) {
-                                bot.sendMessage(chatid, `Proposalid (${proposallist}) status changed to "Finalized" at block ${proposal.finalizedtime()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                                filteredproposal = activeproposals.filter(e => e != proposallist)
-                                tobeexecutedprop.push(proposallist)
-                            } else {
-                                bot.sendMessage(chatid, `Proposalid (${proposallist}) status changed to "Finalized and Executed" at block ${proposal.finalizedtime()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                                filteredproposal = activeproposals.filter(e => e != proposallist)
-                            }
-                            break;
-                        case 'Expired':
-                        case 'Canceled':
-                        case 'Cancelled':
-                        case 'Rejected':
-                        case 'Slashed':
-                        case 'Vetoed':
-                            // console.log(`Proposal ${proposallist} ${propstatus[0]}`)
-                            // bot.sendMessage(chatid, `Proposalid (${proposallist}) status changed to <b>"Finalized:${propstatus[0]}"</b> at block ${proposal.finalizedtime()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                            // filteredproposal = activeproposals.filter(e => e != proposallist)
-                            // break;
-                        default:
-                            console.log(`Proposal ${proposallist} changed to other status: ${propstatus[0]}`)
-                            bot.sendMessage(chatid, `Proposalid (${proposallist}) status changed to <b>"Finalized:${propstatus[0]}"</b> at block ${proposal.finalizedtime()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                            filteredproposal = activeproposals.filter(e => e != proposallist)
-                            break;
-                    }
-                    activeproposals = filteredproposal
-                }
-            } 
-        }
-        if (tobeexecutedprop[0]>0) {
-            for (const proposallist of tobeexecutedprop) {
-                const proposal = await getproposalDetail(api,proposallist)
-                let exestatus = Object.getOwnPropertyNames(proposal.resultfull()['Approved'])[0]
-                if (exestatus=='Executed'){
-                    console.log(`Proposal ${proposallist} has been executed`)
-                    bot.sendMessage(chatid, `Proposalid (${proposallist}) <b>has been executed</b> at block ${proposal.finalizedtime()+proposal.graceperiod()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                    tobeexecutedpropfiltered = tobeexecutedprop.filter(e => e != proposallist)
-                } else {
-                    console.log(`Proposal ${proposallist} Execution is failed`)
-                    bot.sendMessage(chatid, `Proposalid (${proposallist}) <b>failed to be executed</b> at block ${proposal.finalizedtime()+proposal.graceperiod()}.\r\n ${proposal.postmessage()}`, { parse_mode: 'html' })
-                    tobeexecutedpropfiltered = tobeexecutedprop.filter(e => e != proposallist)
-                }    
-                tobeexecutedprop = tobeexecutedpropfiltered
-            }
-        }
-    })
-}
-
-const getpendingProposals = async (api) => {
-    let tobeexecutedprop = ((await api.query.proposalsEngine.pendingExecutionProposalIds()).toJSON())[0]
-    if (tobeexecutedprop[0]==0){
-        return []
-    } else {
-        return tobeexecutedprop
-    }
-}
-
-const getactiveProposals = async (api) => {
-    let activeproposals =  ((await api.query.proposalsEngine.activeProposalIds()).toJSON())[0]
-    if (activeproposals[0]==0){
-        return []
-    } else {
-        return activeproposals
-    }
-}
-
-const getmemberHandle = async (api,memberid) => {
-    const memberprofile = await api.query.members.memberProfile(memberid)
-    const handler = memberprofile.raw.handle.toJSON()
-    return handler
-}
-
-const getproposalStatus = (propresultraw) => {
-    if (propresultraw.hasOwnProperty('proposalStatus')) {
-        return propresultraw.proposalStatus
-    } else {
-        return {Active:null}
-    }
-} 
-
-const getfinalTime = (propresultraw) => {
-    if (propresultraw.hasOwnProperty('finalizedAt')) {
-        return propresultraw.finalizedAt
-    } else {
-        return 0
-    }
-}
-
-const getproposalDetail = async (api,proposalcount) => {
-    const propdetail = await api.query.proposalsEngine.proposals(proposalcount)
-    const parameters = propdetail.parameters
-    const propposterid = propdetail.proposerId.toJSON()
-    const handler = await getmemberHandle(api,propposterid)
-    const proptype = await api.query.proposalsCodex.proposalDetailsByProposalId(proposalcount)
-    const [deftype] = Object.getOwnPropertyNames(proptype.toJSON())
-    const proptitle = propdetail.title.toJSON()
-    const propstage = propdetail.status.toJSON()
-    // const propstatus = propdetail.get("status")
-    const propstatus = Object.getOwnPropertyNames(propstage)
-    const propresultraw =  propstage[propstatus]
-    const propfinalresultfull = getproposalStatus(propresultraw)
-    // const propfinalresultfull = propresultraw.proposalStatus
-    // const propfinalresultjson = Object.getOwnPropertyNames(propresultraw.proposalStatus)
-    const propfinaltime = getfinalTime(propresultraw)
-    // const propfinaltime = propresultraw.finalizedAt
-    const propfinalresultjson = Object.getOwnPropertyNames(propfinalresultfull)
-    const graceperiod = propdetail.parameters.gracePeriod.toNumber()
-    return {
-        detail : function () {
-            return propdetail;
-        },
-        parameters : function () {
-            return parameters;
-        },
-        stage : function () {
-            return propstatus;
-        },
-        finalizedtime : function () {
-            return propfinaltime;
-        },
-        graceperiod : function () {
-            return graceperiod;
-        },
-        resultfull : function () {
-            return propfinalresultfull;
-        },
-        resultjson : function () {
-            return propfinalresultjson;
-        },
-        postmessage : function () {
-            return `<b>Type</b>: ${deftype}\r\n <b>Proposer</b>:<a href="https://testnet.joystream.org/#/members/${handler}"> ${handler}(${propposterid})</a>\r\n <b>Title</b>: <a href="https://testnet.joystream.org/#/proposals/${proposalcount}">${proptitle.substring(0,100)}</a>\r\n <b>Stage</b>: ${propstatus}\r\n <b>Result</b>: ${JSON.stringify(propfinalresultfull)}`;
-        // postmessage : function () {
-        //     return `<b>Type</b>: ${this.deftype()}\r\n <b>Proposer</b>: ${this.handler()}(${this.posterid()})\r\n <b>Title</b>: ${this.title()}\r\n <b>Stage</b>: ${this.stage()}\r\n <b>Result</b>: ${this.result()}`;
-        }
-    }
-}
-
-main()

+ 109 - 0
community-contributions/joystreamtelegrambot/src/bot.ts

@@ -0,0 +1,109 @@
+import TelegramBot from "node-telegram-bot-api";
+import { token, chatid, wsLocation } from "../config";
+
+// types
+import { Options, Proposals } from "./types";
+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";
+import { parseArgs, printStatus, exit } from "./lib/util";
+
+const opts: Options = parseArgs(process.argv.slice(2));
+const log = (msg: string): void | number => opts.verbose && console.log(msg);
+process.env.NTBA_FIX_319 ||
+  log("TL;DR: Set NTBA_FIX_319 to hide this warning.");
+
+const bot = new TelegramBot(token, { polling: true });
+
+const sendMessage = (msg: string) => {
+  try {
+    //bot.sendMessage(chatid, msg, { parse_mode: "HTML" });
+  } catch (e) {
+    console.log(`Failed to send message: ${e}`);
+  }
+};
+
+const main = async () => {
+  const provider = new WsProvider(wsLocation);
+  const api = await ApiPromise.create({ provider, types });
+  await api.isReady;
+
+  log(`Publishing to ${chatid} with token ${token}`);
+
+  const [chain, node, version] = await Promise.all([
+    api.rpc.system.chain(),
+    api.rpc.system.name(),
+    api.rpc.system.version()
+  ]);
+
+  let lastBlock = 0;
+  const cats: number[] = [0, 0];
+  const channels: number[] = [0, 0];
+  const posts: number[] = [0, 0];
+  const threads: number[] = [0, 0];
+  let proposals: Proposals = { last: 0, current: 0, active: [], pending: [] };
+
+  if (opts.channel) channels[0] = await get.currentChannelId(api);
+
+  if (opts.forum) {
+    posts[0] = (await get.currentPostId(api)) - 1;
+    cats[0] = (await get.currentCategoryId(api)) - 1;
+    threads[0] = (await get.currentThreadId(api)) - 1;
+  }
+
+  if (opts.proposals) {
+    proposals.last = (await get.proposalCount(api)) - 1;
+    proposals.active = await get.activeProposals(api);
+    proposals.pending = await get.pendingProposals(api);
+  }
+
+  log(`Subscribed to ${chain} on ${node} v${version}`);
+  const unsubscribe = await api.rpc.chain.subscribeNewHeads(
+    async (block: Header): Promise<void> => {
+      const currentBlock = block.number.toNumber();
+      if (opts.council && currentBlock > lastBlock)
+        lastBlock = await announce.councils(api, currentBlock, sendMessage);
+
+      if (opts.channel) {
+        channels[1] = await get.currentChannelId(api);
+        if (channels[1] > channels[0])
+          channels[0] = await announce.channels(api, channels, sendMessage);
+      }
+
+      if (opts.proposals) {
+        proposals.current = await get.proposalCount(api);
+        if (proposals.current > proposals.last)
+          proposals = await announce.proposals(api, proposals, sendMessage);
+      }
+
+      if (opts.forum) {
+        cats[1] = await get.currentCategoryId(api);
+        posts[1] = await get.currentPostId(api);
+        threads[1] = await get.currentThreadId(api);
+
+        if (cats[1] > cats[0])
+          cats[0] = await announce.categories(api, cats, sendMessage);
+
+        if (posts[1] > posts[0])
+          posts[0] = await announce.posts(api, posts, sendMessage);
+
+        if (threads[1] > threads[0])
+          threads[0] = await announce.threads(api, threads, sendMessage);
+      }
+
+      printStatus(opts, {
+        block: currentBlock,
+        cats,
+        chain: String(chain),
+        posts,
+        proposals,
+        threads
+      });
+    }
+  );
+};
+main().catch(() => exit(log));

+ 237 - 0
community-contributions/joystreamtelegrambot/src/lib/announcements.ts

@@ -0,0 +1,237 @@
+import { Api, Member, ProposalDetail, Proposals } from "../types";
+import { BlockNumber } from "@polkadot/types/interfaces";
+import { Channel, ElectionStage } from "@joystream/types/augment";
+//import { Channel } from "@joystream/types/channel";
+import { Category, Thread, Post } from "@joystream/types/forum";
+import { domain } from "../../config";
+import {
+  categoryById,
+  memberHandle,
+  memberHandleByAccount,
+  proposalDetail
+} from "./getters";
+
+const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+
+const query = async (
+  test: string,
+  callback: () => Promise<any>
+): Promise<any> => {
+  let result: any = await callback();
+  for (let i: number = 0; i < 10; i++) {
+    if (result[test] === "") {
+      console.debug(`refetching ${callback} (${i})`);
+      result = await callback();
+      await sleep(5000);
+    }
+  }
+  return result;
+};
+
+// forum
+
+export const categories = async (
+  api: Api,
+  category: number[],
+  sendMessage: (msg: string) => void
+): Promise<number> => {
+  const messages: string[] = [];
+  let id: number = category[0] + 1;
+  for (id; id <= category[1]; id++) {
+    const category: Category = await query("title", () =>
+      categoryById(api, id)
+    );
+    messages.push(
+      `Category ${id}: <b><a href="${domain}/#/forum/categories/${id}">${category.title}</a></b>`
+    );
+  }
+  sendMessage(messages.join("\r\n\r\n"));
+  return category[1];
+};
+
+export const channels = async (
+  api: Api,
+  channels: number[],
+  sendMessage: (msg: string) => void
+): Promise<number> => {
+  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, handle: "", url: "" };
+    member.handle = await memberHandle(api, member.id.toJSON());
+    member.url = `${domain}/#/members/${member.handle}`;
+    messages.push(
+      `<b>Channel <a href="${domain}/#//media/channels/${id}">${channel.title}</a> by <a href="${member.url}">${member.handle} (${member.id})</a></b>`
+    );
+  }
+  sendMessage(messages.join("\r\n\r\n"));
+  return current;
+};
+
+export const councils = async (
+  api: Api,
+  block: number,
+  sendMessage: (msg: string) => void
+): Promise<number> => {
+  let current: number = block;
+  const round: number = await api.query.councilElection.round();
+  const stage: ElectionStage | null = await await api.query.councilElection.stage();
+  if (!stage) {
+    const councilEnd: BlockNumber = await api.query.council.termEndsAt();
+    current = councilEnd.toNumber();
+    const termDuration: BlockNumber = await api.query.councilElection.newTermDuration();
+    const block = current - termDuration.toNumber();
+    sendMessage(
+      `<a href="${domain}/#/council/members">Council for round ${round}</a> has been elected at block ${block} until block ${councilEnd}.`
+    );
+  } else {
+    if (stage.isAnnouncing) {
+      current = stage.asAnnouncing.toNumber();
+      const announcingPeriod: BlockNumber = await api.query.councilElection.announcingPeriod();
+      const block = current - announcingPeriod.toNumber();
+      sendMessage(
+        `Announcing election for round ${round} at ${block}.<a href="${domain}/#/council/applicants">Apply now!</a>`
+      );
+    }
+
+    if (stage.isVoting) {
+      current = stage.asVoting.toNumber();
+      const votingPeriod: BlockNumber = await api.query.councilElection.votingPeriod();
+      const block = current - votingPeriod.toNumber();
+      sendMessage(
+        `Voting stage for council election started at block ${block}. <a href="${domain}/#/council/applicants">Vote now!</a>`
+      );
+    }
+
+    if (stage.isRevealing) {
+      current = stage.asRevealing.toNumber();
+      const revealingPeriod: BlockNumber = await api.query.councilElection.revealingPeriod();
+      const block = current - revealingPeriod.toNumber();
+      sendMessage(
+        `Revealing stage for council election started at block ${block}. <a href="${domain}/#/council/votes">Don't forget to reveal your vote!</a>`
+      );
+    }
+  }
+  return current;
+};
+
+export const posts = async (
+  api: Api,
+  posts: number[],
+  sendMessage: (msg: string) => void
+): Promise<number> => {
+  const [last, current] = posts;
+  const messages: string[] = [];
+  let id: number = last + 1;
+  for (id; id <= current; id++) {
+    const post: Post = await query("current_text", () =>
+      api.query.forum.postById(id)
+    );
+    const replyId: number = post.nr_in_thread.toNumber();
+    const message: string = post.current_text;
+    const excerpt: string = message.substring(0, 100);
+    const threadId: number = post.thread_id.toNumber();
+    const thread: Thread = await query("title", () =>
+      api.query.forum.threadById(threadId)
+    );
+    const threadTitle: string = thread.title;
+    const category: Category = await query("title", () =>
+      categoryById(api, thread.category_id.toNumber())
+    );
+    const handle = await memberHandleByAccount(api, post.author_id.toJSON());
+    messages.push(
+      `<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>`
+    );
+  }
+  sendMessage(messages.join("\r\n\r\n"));
+  return current;
+};
+
+const processActive = async (
+  id: number,
+  details: ProposalDetail,
+  sendMessage: (s: string) => void
+): Promise<boolean> => {
+  const { createdAt, finalizedAt, message, parameters, result } = details;
+  let msg = `Proposal ${id} <b>created</b> at block ${createdAt}.\r\n${message}`;
+  if (details.stage === "Finalized") {
+    let label: string = result;
+    if (result === "Approved") {
+      const executed = parameters.gracePeriod.toNumber() > 0 ? false : true;
+      label = executed ? "Finalized" : "Finalized and Executed";
+    }
+    msg = `Proposal ${id} <b>${label}</b> at block ${finalizedAt}.\r\n${message}`;
+    sendMessage(msg);
+    return true;
+  } else return processPending(id, details, sendMessage);
+};
+
+const processPending = async (
+  id: number,
+  details: ProposalDetail,
+  sendMessage: (s: string) => void
+): Promise<boolean> => {
+  const { createdAt, message, parameters, stage } = details;
+  if (stage === "Finalized") return processActive(id, details, sendMessage);
+  const votingEndsAt = createdAt + parameters.votingPeriod.toNumber();
+  const msg = `Proposal ${id} <b>created</b> at block ${createdAt}.\r\n${message}\r\nYou can vote until block ${votingEndsAt}.`;
+  sendMessage(msg);
+  return true;
+};
+
+export const proposals = async (
+  api: Api,
+  prop: Proposals,
+  sendMessage: (msg: string) => void
+): Promise<Proposals> => {
+  let { current, last, active, pending } = prop;
+
+  for (let id: number = last++; id <= current; id++) active.push(id);
+
+  for (const id of active)
+    if (processActive(id, await proposalDetail(api, id), sendMessage))
+      active = active.filter((e: number) => e !== id);
+
+  for (const id of pending)
+    if (processPending(id, await proposalDetail(api, id), sendMessage))
+      pending = pending.filter((e: number) => e !== id);
+
+  return { current, last: current, active, pending };
+};
+
+export const threads = async (
+  api: Api,
+  threads: number[],
+  sendMessage: (msg: string) => void
+): Promise<number> => {
+  const [last, current] = threads;
+  const messages: string[] = [];
+  let id: number = last + 1;
+  for (id; id <= current; id++) {
+    const thread: Thread = await query("title", () =>
+      api.query.forum.threadById(id)
+    );
+    const { title, author_id } = thread;
+    const memberName: string = await memberHandleByAccount(
+      api,
+      author_id.toJSON()
+    );
+    const category: Category = await query("title", () =>
+      categoryById(api, thread.category_id.toNumber())
+    );
+    messages.push(
+      `Thread ${id}: <a href="${domain}/#/forum/threads/${id}">"${title}"</a> by <a href="${domain}/#/members/${memberName}">${memberName}</a> in category "<a href="${domain}/#/forum/categories/${category.id}">${category.title}</a>" `
+    );
+  }
+  sendMessage(messages.join("\r\n\r\n"));
+  return id;
+};
+
+export const formatProposalMessage = (data: string[]): string => {
+  const [id, title, type, stage, result, memberHandle] = data;
+  return `<b>Type</b>: ${type}\r\n<b>Proposer</b>:<a href="${domain}/#/members/${memberHandle}"> ${memberHandle}</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}`;
+};

+ 137 - 0
community-contributions/joystreamtelegrambot/src/lib/getters.ts

@@ -0,0 +1,137 @@
+import { Api, Proposals, ProposalArray, ProposalDetail } from "../types";
+import {
+  ChannelId,
+  ElectionStage,
+  PostId,
+  ProposalDetailsOf,
+  ThreadId
+} from "@joystream/types/augment";
+import { Category, CategoryId } from "@joystream/types/forum";
+import { MemberId, Membership } from "@joystream/types/members";
+import { Proposal, ProposalStatus } from "@joystream/types/proposals";
+
+import { formatProposalMessage } from "./announcements";
+import { domain } from "../../config";
+
+// channel
+
+export const currentChannelId = async (api: Api): Promise<number> => {
+  const id: ChannelId = await api.query.contentWorkingGroup.nextChannelId();
+  return id.toNumber() - 1;
+};
+
+export const memberHandle = async (api: Api, id: number): 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: number = 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> => {
+  const proposalCount: number = await api.query.proposalsEngine.proposalCount();
+  return proposalCount || 0;
+};
+
+const activeProposalCount = async (api: Api): Promise<number> => {
+  const proposalCount: number = await api.query.proposalsEngine.activeProposalCount();
+  return proposalCount || 0;
+};
+
+export const pendingProposals = async (api: Api): Promise<ProposalArray> => {
+  const pending: ProposalArray = await api.query.proposalsEngine.pendingExecutionProposalIds(
+    await activeProposalCount(api)
+  );
+  //const pending: ProposalArray = pendingProposals.toJSON();
+  if (pending.length) console.debug("pending proposals", pending);
+  return pending;
+};
+
+export const activeProposals = async (api: Api): Promise<ProposalArray> => {
+  const active: ProposalArray = await api.query.proposalsEngine.activeProposalIds(
+    await activeProposalCount(api)
+  );
+  //const active: ProposalArray = result.toJSON();
+  if (active.length) console.debug("active proposals", active);
+  return active;
+};
+
+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> => {
+  const proposal: Proposal = await api.query.proposalsEngine.proposals(id);
+  const { parameters, proposerId, description } = proposal;
+  const author: string = await memberHandle(api, proposerId.toNumber());
+  const createdAt: number = proposal.createdAt.toNumber();
+  const title: string = proposal.title.toString();
+  const proposerHandle: string = await memberHandle(api, proposerId.toJSON());
+  const status: { [key: string]: any } = proposal.status;
+  const stage: string = status.isActive ? "Active" : "Finalized";
+  const { finalizedAt, proposalStatus } = status[`as${stage}`];
+  const type: string = await getProposalType(api, id);
+  const result: string = proposalStatus
+    ? (proposalStatus.isApproved && "Approved") ||
+      (proposalStatus.isCanceled && "Canceled") ||
+      (proposalStatus.isExpired && "Expired") ||
+      (proposalStatus.isRejected && "Rejected") ||
+      (proposalStatus.isSlashed && "Slashed") ||
+      (proposalStatus.isVetoed && "Vetoed")
+    : "Pending";
+
+  const message: string = formatProposalMessage([
+    String(id),
+    title,
+    type,
+    stage,
+    result,
+    author
+  ]);
+  const proposalDetail: ProposalDetail = {
+    createdAt,
+    finalizedAt,
+    parameters,
+    message,
+    stage,
+    result
+  };
+  return proposalDetail;
+};

+ 51 - 0
community-contributions/joystreamtelegrambot/src/lib/util.ts

@@ -0,0 +1,51 @@
+import { Options } from "../types";
+import { 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;
+    cats: number[];
+    chain: string;
+    posts: number[];
+    proposals: Proposals;
+    threads: number[];
+  }
+): void => {
+  if (opts.verbose < 1) return;
+
+  const { block, chain, proposals, cats, posts, threads } = data;
+  const date = moment().format("L HH:mm:ss");
+  let message = `[${date}] Chain:${chain} Block:${block} `;
+
+  if (opts.forum)
+    message += `Post:${posts[1]} Cat:${cats[1]} Thread:${threads[1]} `;
+
+  if (opts.proposals)
+    message += `Proposals:${proposals.current} (Active:${proposals.active.length} Pending:${proposals.pending.length}) `;
+
+  console.log(message);
+};
+
+export const exit = (log: (s: string) => void) => {
+  log("\nNo connection, exiting.\n");
+  process.exit();
+};

+ 93 - 0
community-contributions/joystreamtelegrambot/src/tests.ts

@@ -0,0 +1,93 @@
+//import TelegramBot from "node-telegram-bot-api";
+import { wsLocation } from "../config";
+
+// types
+import { Proposals } from "./types";
+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): void => console.log(msg);
+const sendMessage = log;
+
+const main = async () => {
+  const provider = new WsProvider(wsLocation);
+  const api = await ApiPromise.create({ provider, types });
+  await api.isReady;
+
+  const [chain, node, version] = await Promise.all([
+    api.rpc.system.chain(),
+    api.rpc.system.name(),
+    api.rpc.system.version()
+  ]);
+  log(`Connected to ${chain} on ${node} v${version}`);
+
+  let lastBlock = 0;
+  let proposals: Proposals = {
+    last: 1,
+    current: 2,
+    active: [],
+    pending: []
+  };
+  let categories = [0, 0];
+  let posts = [0, 0];
+  let threads = [0, 0];
+  let channels = [0, 0];
+
+  const unsubscribe = await api.rpc.chain.subscribeNewHeads(
+    async (block: Header): Promise<void> => {
+      // council
+      if (lastBlock > 0) process.exit;
+      lastBlock = block.number.toNumber();
+      const currentBlock = block.number.toNumber();
+      log("current council");
+      announce.councils(api, currentBlock, sendMessage);
+
+      log("first proposal");
+      announce.proposals(api, proposals, sendMessage);
+
+      log("last proposal");
+      proposals.current = await get.proposalCount(api);
+      proposals.last = proposals.current - 1;
+      announce.proposals(api, proposals, sendMessage);
+
+      log("first category");
+      announce.categories(api, categories, sendMessage);
+
+      log("last category");
+      categories[1] = await get.currentCategoryId(api);
+      categories[0] = categories[1] - 1;
+      announce.categories(api, categories, sendMessage);
+
+      log("first post");
+      announce.posts(api, posts, sendMessage);
+
+      log("last post");
+      posts[1] = await get.currentPostId(api);
+      posts[0] = posts[1] - 1;
+      announce.posts(api, posts, sendMessage);
+
+      log("first thread");
+      announce.threads(api, threads, sendMessage);
+
+      log("last thread");
+      threads[1] = await get.currentThreadId(api);
+      threads[0] = threads[1] - 1;
+      announce.threads(api, threads, 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);
+    }
+  );
+};
+
+main();

+ 40 - 0
community-contributions/joystreamtelegrambot/src/types/index.ts

@@ -0,0 +1,40 @@
+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";
+
+export interface Api {
+  query: any;
+}
+
+export interface Options {
+  verbose: number;
+  channel: boolean;
+  council: boolean;
+  forum: boolean;
+  proposals: boolean;
+}
+
+export interface ProposalDetail {
+  createdAt: number;
+  finalizedAt: number;
+  message: string;
+  parameters: ProposalParameters;
+  stage: string;
+  result: string;
+}
+
+export type ProposalArray = number[];
+
+export interface Proposals {
+  current: number;
+  last: number;
+  active: ProposalArray;
+  pending: ProposalArray;
+}
+
+export interface Member {
+  id: MemberId;
+  handle: string;
+  url?: string;
+}

+ 69 - 0
community-contributions/joystreamtelegrambot/tsconfig.json

@@ -0,0 +1,69 @@
+{
+  "compilerOptions": {
+    /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+    /* Basic Options */
+    // "incremental": true,                   /* Enable incremental compilation */
+    "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
+    "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
+    // "lib": [],                             /* Specify library files to be included in the compilation. */
+    // "allowJs": true,                       /* Allow javascript files to be compiled. */
+    // "checkJs": true,                       /* Report errors in .js files. */
+    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
+    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
+    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
+    // "outFile": "./",                       /* Concatenate and emit output to single file. */
+    "outDir": "./dist" /* Redirect output structure to the directory. */,
+    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    // "composite": true,                     /* Enable project compilation */
+    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
+    // "removeComments": true,                /* Do not emit comments to output. */
+    // "noEmit": true,                        /* Do not emit outputs. */
+    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
+    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+    /* Strict Type-Checking Options */
+    "strict": true /* Enable all strict type-checking options. */,
+    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,              /* Enable strict null checks. */
+    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
+    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
+    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
+    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
+
+    /* Additional Checks */
+    // "noUnusedLocals": true,                /* Report errors on unused locals. */
+    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
+    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
+    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
+
+    /* Module Resolution Options */
+    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
+    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
+    // "typeRoots": [],                       /* List of folders to include type definitions from. */
+    // "types": [],                           /* Type declaration files to be included in compilation. */
+    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
+    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
+    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */
+
+    /* Source Map Options */
+    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
+    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+    /* Experimental Options */
+    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
+    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
+
+    /* Advanced Options */
+    "skipLibCheck": true /* Skip type checking of declaration files. */,
+    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
+  }
+}