diff --git a/ProcFile b/Procfile similarity index 100% rename from ProcFile rename to Procfile diff --git a/README.md b/README.md index e80ea71..c883166 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ **indev** - still being made -Telegram bot. +Telegram bot deployed using Heroku. + +## Why + +A test on async await, and typescript. Since this project is IO-bound(mostly), it makes sense to use Node and Typescript. ## Usage @@ -20,6 +24,6 @@ JACK_TOKEN="u thought" npm start ## Goals - [X] Read Name - - [ ] Read msgs - - [ ] Deal with msgs + - [X] Read msgs + - [X] Deal with msgs - [ ] Implement response bodies \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index cd59d82..c74320a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,8 @@ import delay from './misc/delay' import {teleargs,envVars,getUpdateBody} from './misc/defs' import getName from './misc/getName' import {getMessage,getInit} from './tg/getWrapper'; +import { replyStrategy} from './tg/replyStrategy'; +import {sendWrapper} from './tg/sendWrapper' import { assert } from 'console'; const token = process.env['JACK_TOKEN'] as string; @@ -10,15 +12,15 @@ if(token===undefined){ } if (require.main === module) { - loop({token}).catch(reason => { - console.error("E:", reason); + console.error('E:', reason); }); } -//Enable this to see cool viz -// setInterval(()=>console.log("10secs"),10000); - +/** + * Main bot loop + * @param args Environment variables + */ async function loop(args:envVars) { const requestURL = 'https://api.telegram.org/bot'+token; let name:string; @@ -31,19 +33,41 @@ async function loop(args:envVars) { console.info(`Name: ${selfName}`); let lastUpdate = 0; - const timeout = 100; + + //configurable parameters + const pollingInterval = 100; + const maxArraySize = 64; + const intervalTimerVal = 1000; + + const sender = new sendWrapper(requestURL,intervalTimerVal/10,maxArraySize); + const requestArrays:Promise[] = []; try { while (true) { //basically, check every second at max - let x = delay(1000); + let x = delay(intervalTimerVal); //while we await, we can do other stuff - lastUpdate = await main({name:selfName,token,requestURL,lastUpdate,timeout}); + const responses= await main({name:selfName,token,requestURL,lastUpdate,timeout: pollingInterval}); + //update the last response number + lastUpdate = responses.maxMsgUpdateID; + //push it all in + await sender.pushAll(responses.sendables); + + requestArrays.push(sender.popMost()); + + + //~~ makes it an integer + if(requestArrays.length>~~(maxArraySize/10)){ + //ensure the request some time ago has completed. + //If it hasnt, it should stall everything + await requestArrays.shift(); + } + await x; } }catch(e){ - console.error("E:",e.msg||e.message||'Error'); - setTimeout(loop,timeout*101); + console.error('E:',e.msg||e.message||'Error'); + setTimeout(loop,pollingInterval*101); } } @@ -53,7 +77,7 @@ async function loop(args:envVars) { * @returns the last update we got */ async function main(args:teleargs) { - console.debug("Logging Update for:",args.lastUpdate); + // console.debug("Logging Update for:",args.lastUpdate); const actions = await getMessage(args); const body = actions.body as unknown as getUpdateBody; assert(body.ok); @@ -61,14 +85,15 @@ async function main(args:teleargs) { //Do something - console.debug(body.result); + // console.debug(body.result); + const sendables = await replyStrategy(body.result); - // const update_id = actions.body. + // return the update number - const getMaxMsgUpdateID:number = body.result.reduce((acc,curr)=>{ + const maxMsgUpdateID:number = body.result.reduce((acc,curr)=>{ return curr.update_id>acc?curr.update_id:acc; },args.lastUpdate); - return getMaxMsgUpdateID; + return {maxMsgUpdateID,sendables}; } diff --git a/src/misc/defs.ts b/src/misc/defs.ts index 73a1108..5ef5507 100644 --- a/src/misc/defs.ts +++ b/src/misc/defs.ts @@ -12,9 +12,24 @@ export interface envVars{ } - +export interface userObj{ + id:number, + is_bot:boolean, + first_name:string, + last_name?:string, + username?:string +} export interface messageObj{ - + message_id:number, + from?:userObj,//TODOF add user + date:number, + chat:any,//TODOF add Chat + audio?:any, + document?:any, + photo?:any, + sticker?:any, + caption?:string, + text?:string } export interface getUpdateResultBody{ @@ -26,17 +41,14 @@ export interface getUpdateBody{ result:getUpdateResultBody[] } - +export interface sendMessage{ + chat_id:number, + text:string, + disable_notification?:boolean, + reply_to_message_id?: number +} export interface initBody{ ok:boolean, - result:{ - id:number, - is_bot:boolean, - first_name:string, - username:string, - can_join_groups:boolean, - can_read_all_group_messages:boolean, - supports_inline_queries: boolean - } + result:userObj } \ No newline at end of file diff --git a/src/tg/replyStrategy.ts b/src/tg/replyStrategy.ts new file mode 100644 index 0000000..49aa68c --- /dev/null +++ b/src/tg/replyStrategy.ts @@ -0,0 +1,48 @@ + +import { getUpdateResultBody, sendMessage } from '../misc/defs'; + + + +/** + * Filter the non-text out + * @param updates set of updates + */ +export async function replyStrategy(updates: getUpdateResultBody[]) { + const nonTextMessages = updates.filter(e => e.message && !e.message.text); + + + + const textMessages = updates.filter(e => e.message && e.message.text); + + //Log stats + if (textMessages.length > 0||nonTextMessages.length > 0) { + console.log(`Got: ${textMessages.length} text messages,${nonTextMessages.length} weird stuff`); + } + + const textReplies = await replyText(textMessages); + const nonTextReplies = await replyNonText(nonTextMessages); + + return [...textReplies,...nonTextReplies]; +} + + +export async function replyText(updates: getUpdateResultBody[]): Promise { + return updates.map(e => { + return { + chat_id: e.message?.from?.id as number, + reply_to_message_id: e.message?.message_id, + text: 'Hello World!' + }; + }) + +} + +export async function replyNonText(updates: getUpdateResultBody[]):Promise { + return updates.map(e => { + return { + chat_id: e.message?.from?.id as number, + text: 'This is useless to me :(', + reply_to_message_id: e.message?.message_id + }; + }) +} \ No newline at end of file diff --git a/src/tg/sendWrapper.ts b/src/tg/sendWrapper.ts new file mode 100644 index 0000000..a7ee038 --- /dev/null +++ b/src/tg/sendWrapper.ts @@ -0,0 +1,76 @@ +import { sendMessage } from '../misc/defs' +import got from 'got'; +import assert from 'assert'; +import delay from '../misc/delay' + + +/** + * Buffers the output + */ +export class sendWrapper { + queue: sendMessage[]; + maxLength: number; + sendURL: string; + minDelay: number; + /** + * + * @param requestURL request URL to send via + * @param minDelay Minimum delay between + * @param maxLength Max length of allowed array + */ + constructor(requestURL: string, minDelay: number = 500, maxLength: number = 100) { + this.queue = []; + this.minDelay = minDelay; + this.sendURL = requestURL + '/sendMessage'; + this.maxLength = maxLength; + } + + /** + * + * @param message Message to send + * @returns Whether push was successful + */ + public async push(message: sendMessage) { + if (this.queue.length < this.maxLength) { + this.queue.push(message); + return true; + } else { + console.warn("Too many responses"); + return false; + } + } + + + public async pushAll(messages:sendMessage[]){ + if (this.queue.length+messages.length < this.maxLength) { + this.queue.push(...messages); + } else { + //best fit + this.queue.push(...messages.slice(0,this.maxLength-messages.length)); + console.warn("Too many responses"); + } + return this; + } + + /** + * Send all required + */ + public async popMost(): Promise { + while (this.queue.length > 0) { + //Removing this + const element = this.queue.shift() + if (element) { + const mintimer = delay(this.minDelay); + try { + const answ = await got.post(this.sendURL, { json: element }); + }catch(e){ + console.error("Failed to send:",element.text); + return false; + }finally{ + await mintimer; + } + } + } + return true; + } +} \ No newline at end of file