import { Contact , Friendship , Message , Room , RoomInvitation , Wechaty , WechatyBuilder , } from 'wechaty' ; import { Context , Markup , Telegraf } from 'telegraf' ; import { Message as TTMessage , Update as TTUpdate , UserFromGetMe , } from 'telegraf/typings/core/types/typegram' ; import { handleCurrent , handleFind , handleForwardTo , handleLock , handleMute , handleTelegramMessage , handleUnlock , handleUnmute , handleWechatMessage , } from './bot/index' ; import HTMLTemplates from './lib/HTMLTemplates' ; import { HttpsProxyAgent } from 'https-proxy-agent' ; import Logger from './lib/Logger' ; import MiscHelper from './lib/MiscHelper' ; import { SocksProxyAgent } from 'socks-proxy-agent' ; import crypto from 'crypto' ; import dayjs from 'dayjs' ; import { findContact } from './bot/HandleFindX' ; import lang from './strings' ; import qr from 'qr-image' ; import { readFile } from './bot/UpdateTmpFile' ; import relativeTime from 'dayjs/plugin/relativeTime' ; const { version } = require ( '../../package.json' ) ; dayjs . extend ( relativeTime ) ; export interface BotOptions { token : string ; padplusToken ? : string ; socks5Proxy ? : { host : string ; port : number ; username ? : string ; password ? : string ; } ; httpProxy ? : { host : string ; port : number ; } ; keepMsgs ? : number ; silent ? : boolean ; } export interface Client { wechat ? : Wechaty ; receiveGroups ? : boolean ; receiveOfficialAccount ? : boolean ; receiveSelf ? : boolean ; msgs : Map < number , { contact : Contact | Room ; wxmsg : Message ; } > ; currentContact ? : Room | Contact ; contactLocked ? : boolean ; wechatId ? : string ; initialized ? : boolean ; botId : string ; firstMsgId ? : any ; muteList : string [ ] ; } export default class Bot { clients : Map < number , Client > = new Map ( ) ; keepMsgs : number ; options : BotOptions ; protected bot : Telegraf < Context > ; protected botSelf : UserFromGetMe ; protected beforeCheckUserList : ( ( ctx ? : Context ) => Promise < boolean > ) [ ] = [ ] ; protected pendingFriends = new Map < string , Friendship > ( ) ; protected lastRoomInvitation : RoomInvitation = null ; private recoverWechats = new Map < number , Wechaty > ( ) ; readonly id : string ; readonly uptime = dayjs ( ) ; constructor ( options ) { this . options = options ; const botid = crypto . createHash ( 'sha256' ) . update ( options . token || options . padplusToken ) . digest ( ) . toString ( 'hex' ) . substring ( 0 , 4 ) ; this . id = `leavexchat_${ botid }.` ; const { token , socks5Proxy , keepMsgs , httpProxy } = options ; this . keepMsgs = keepMsgs === undefined ? 200 : Math . max ( keepMsgs , 100 ) || 200 ; const socks5agent = socks5Proxy ? new SocksProxyAgent ( `socks5://${ socks5Proxy . host }:${ socks5Proxy . port }` ) : undefined ; const agent = httpProxy ? new HttpsProxyAgent ( `http://${ httpProxy . host }:${ httpProxy . port }` ) : undefined ; this . bot = new Telegraf ( token , { telegram : { agent : agent || socks5agent } , } ) ; const checkUser = ( ctx , n ) => this . checkUser ( ctx , n ) ; const replyOk = ( ctx ) => ctx . reply ( 'OK' ) ; const turnGroup = ( ctx , n , on ) => { ctx [ 'user' ] . receiveGroups = on ; n ( ) ; } ; const turnOfficial = ( ctx , n , on ) => { ctx [ 'user' ] . receiveOfficialAccount = on ; n ( ) ; } ; const turnSelf = ( ctx , n , on ) => { ctx [ 'user' ] . receiveSelf = on ; n ( ) ; } ; this . bot . start ( this . handleStart ) ; this . bot . command ( 'version' , ( ctx ) => ctx . reply ( `Bot version: ${ version }` ) ) ; this . bot . command ( 'stop' , checkUser , this . handleLogout ) ; this . bot . command ( 'login' , ( ctx ) => this . handleLogin ( ctx ) ) ; this . bot . command ( 'shutdown' , ( _ ) => process . exit ( 0 ) ) ; const turnGroupOn = ( ctx , n ) => turnGroup ( ctx , n , true ) ; this . bot . command ( 'groupon' , checkUser , turnGroupOn , replyOk ) ; const turnGroupOff = ( ctx , n ) => turnGroup ( ctx , n , false ) ; this . bot . command ( 'groupoff' , checkUser , turnGroupOff , replyOk ) ; const turnOfficialOn = ( ctx , n ) => turnOfficial ( ctx , n , true ) ; this . bot . command ( 'officialon' , checkUser , turnOfficialOn , replyOk ) ; const turnOfficialOff = ( ctx , n ) => turnOfficial ( ctx , n , false ) ; this . bot . command ( 'officialoff' , checkUser , turnOfficialOff , replyOk ) ; const turnSelfOn = ( ctx , n ) => turnSelf ( ctx , n , true ) ; this . bot . command ( 'selfon' , checkUser , turnSelfOn , replyOk ) ; const turnSelfOff = ( ctx , n ) => turnSelf ( ctx , n , false ) ; this . bot . command ( 'selfoff' , checkUser , turnSelfOff , replyOk ) ; const handleUpTime = ( ctx ) => { ctx . replyWithHTML ( `<code>${ this . uptime . toISOString ( ) }  [${ dayjs ( ) . from ( this . uptime , true ) }]</code>` , { reply_to_message_id : ctx [ 'user' ] . firstMsgId , } ) ; } ; this . bot . command ( 'uptime' , checkUser , handleUpTime ) ; this . bot . command ( 'find' , checkUser , this . handleFind ) ; this . bot . command ( 'lock' , checkUser , this . handleLock ) ; this . bot . command ( 'unlock' , checkUser , this . handleUnlock ) ; this . bot . command ( 'findandlock' , checkUser , this . handleFind , this . handleLock ) ; this . bot . command ( 'current' , checkUser , this . handleCurrent ) ; this . bot . command ( 'agree' , checkUser , this . handleAgreeFriendship ) ; this . bot . command ( 'disagree' , checkUser , this . handleDisagreeFriendship ) ; this . bot . command ( 'acceptroom' , checkUser ) ; this . bot . command ( 'forward' , checkUser , this . handleForward ) ; this . bot . command ( 'forwardto' , checkUser , this . handleForward ) ; this . bot . command ( 'mute' , checkUser , this . handleMute ) ; this . bot . command ( 'unmute' , checkUser , this . handleUnmute ) ; this . bot . command ( 'quitroom' , checkUser , this . handleQuitRoom ) ; this . bot . command ( 'logout' , checkUser , this . handleLogout ) ; this . bot . help ( ( ctx ) => ctx . reply ( lang . help ) ) ; this . bot . catch ( ( err ) => { Logger . error ( 'Ooops' , err ?. [ 'message' ] ) ; } ) ; } handleFatalError = async ( err ) => Logger . error ( `Bot Alert: ${ err }` ) ; sendSystemMessage = async ( msg ) => { if ( this . options . silent ) return ; const alert = HTMLTemplates . message ( { nickname : `[Bot Alert]` , message : msg , } ) ; for ( let [ id , _ ] of this . clients ) { await this . bot . telegram . sendMessage ( id , alert , { parse_mode : 'HTML' } ) ; } } ; async launch ( ) { this . bot . on ( 'message' , ( ctx , n ) => this . checkUser ( ctx , n ) , this . handleTelegramMessage ) ; await this . bot . launch ( ) ; this . botSelf = await this . bot . telegram . getMe ( ) ; Logger . info ( `Bot is running` ) ; await this . recoverSessions ( ) ; } async recoverSessions ( ) { const files = await MiscHelper . listTmpFile ( this . id ) ; const ids = files . map ( ( f ) => f . split ( '.' ) [ 1 ] ) . filter ( ( s ) => s ) . map ( ( s ) => Number . parseInt ( s ) ) ; Logger . info ( `Recovering ${ ids . length } sessions...` ) ; await Promise . all ( ids . map ( async ( chatid ) => { const client = this . createClient ( chatid ) ; const { wechat } = client ; wechat . once ( 'login' , async ( user ) => { client . wechatId = user . id ; const ctx = new Context ( { message : { chat : { id : chatid } } } as TTUpdate , this . bot . telegram , this . botSelf ) ; await this . handleLogin ( ctx ) ; const alert = `<code>${ lang . login . sessionOK }</code>` ; await this . bot . telegram . sendMessage ( chatid , alert , { parse_mode : 'HTML' , } ) ; const lastDump = await readFile ( `${ this . id }${ chatid }` ) ; if ( lastDump . recentContact && lastDump . recentContact . name ) { const { found , foundName } = await findContact ( lastDump . recentContact . name , wechat ) ; client . currentContact = found ; client . contactLocked = lastDump . recentContact . locked ; if ( found ) { await ctx . reply ( client . contactLocked ? lang . message . contactLocked ( foundName ) : lang . message . contactFound ( foundName ) ) ; } else { client . contactLocked = false ; } } client . muteList = lastDump . muteList || [ ] ; this . recoverWechats . delete ( chatid ) ; } ) ; const deleteWechaty = async ( ) => { wechat . reset ( ) ; this . clients . delete ( chatid ) ; this . recoverWechats . delete ( chatid ) ; const alert = HTMLTemplates . message ( { nickname : `[Bot Alert]` , message : lang . login . sessionLost , } ) ; await this . bot . telegram . sendMessage ( chatid , alert , { parse_mode : 'HTML' , } ) ; await wechat ?. stop ( ) ; await MiscHelper . deleteTmpFile ( `${ this . id }${ chatid }` ) ; } ; wechat . once ( 'scan' , async ( _ ) => await deleteWechaty ( ) ) ; wechat . once ( 'error' , async ( _ ) => await deleteWechaty ( ) ) ; this . recoverWechats . set ( chatid , wechat ) ; await wechat . start ( ) ; } ) ) ; } async exit ( ) { for ( let [ _ , client ] of this . clients ) { await client . wechat ?. logout ( ) ; await client . wechat ?. stop ( ) ; } } protected handleStart = async ( ctx ) => { await ctx . reply ( lang . welcome ) . catch ( ) ; await ctx . reply ( lang . help ) ; } ; private createClient ( chatid ) { if ( this . clients . has ( chatid ) ) return this . clients . get ( chatid ) ; let wechat = this . recoverWechats . get ( chatid ) || WechatyBuilder . build ( { name : `telegram_${ chatid })}` , puppet : 'wechaty-puppet-wechat' , } ) ; let client = { wechat , msgs : new Map ( ) , receiveGroups : true , receiveOfficialAccount : true , muteList : [ ] , botId : this . id , } ; this . clients . set ( chatid , client ) ; return client ; } protected async handleLogin ( ctx ) { for ( let c of this . beforeCheckUserList ) { if ( ! ( await c ( ctx ) ) ) return ; } const id = ctx . chat . id ; let qrcodeCache = '' ; if ( this . clients . has ( id ) && this . clients . get ( id ) ?. initialized ) { let user = this . clients . get ( id ) ; if ( user . wechatId ) { ctx . reply ( lang . login . logined ( user . wechat . ContactSelf . name ) ) ; return ; } ctx . reply ( lang . login . retry ) ; return ; } const client = this . createClient ( id ) ; const { wechat } = client ; let loginTimer ; let qrMessage = undefined ; const removeQRMessage = async ( ) => { if ( ! qrMessage ) return ; await this . bot . telegram . deleteMessage ( id , qrMessage . message_id ) ; qrMessage = undefined ; } ; const deleteWechat = async ( { clean } = { clean : true } ) => { this . clients . delete ( id ) ; await wechat ?. stop ( ) . catch ( ) ; await removeQRMessage ( ) ; if ( clean ) await MiscHelper . deleteTmpFile ( `${ this . id }${ id }` ) ; } ; const handleQrcode = async ( qrcode ) => { if ( qrcode === qrcodeCache ) return ; qrcodeCache = qrcode ; if ( client . wechatId ) return ; if ( ! loginTimer ) { loginTimer = setTimeout ( async ( ) => { await deleteWechat ( ) ; ctx . reply ( lang . message . timeout ) ; await removeQRMessage ( ) ; } , 3 * 60 * 1000 ) ; } await removeQRMessage ( ) ; qrMessage = await ctx . replyWithPhoto ( { source : qr . image ( qrcode ) } ) . catch ( ( ) => deleteWechat ( ) ) ; } ; wechat ?. on ( 'scan' , handleQrcode ) ; wechat ?. once ( 'login' , async ( user ) => { this . clients . get ( id ) . wechatId = user . id ; await ctx . reply ( lang . login . logined ( user . name ( ) ) ) ; clearTimeout ( loginTimer ) ; wechat ?. off ( 'scan' , handleQrcode ) ; await removeQRMessage ( ) ; await MiscHelper . createTmpFile ( `${ this . id }${ id }` ) ; } ) ; wechat ?. on ( 'friendship' , async ( req ) => { let hello = req . hello ( ) ; let contact = req . contact ( ) ; let name = contact . name ( ) ; if ( req . type ( ) === 2 ) { let avatar = await ( await contact . avatar ( ) ) . toStream ( ) ; await ctx . replyWithPhoto ( { source : avatar } , { caption : `[${ lang . contact . friend }]\n\n${ hello }` , parse_mode : 'MarkdownV2' , reply_markup : undefined , } ) ; this . pendingFriends . set ( name . toLowerCase ( ) , req ) ; } } ) ; wechat ?. on ( 'room-invite' , async ( invitation ) => { let inviter = ( await invitation . inviter ( ) ) . name ( ) ; let topic = await invitation . topic ( ) ; await ctx . reply ( `${ lang . message . inviteRoom ( inviter , topic ) } /acceptroom` ) ; } ) ; wechat ?. on ( 'logout' , async ( user ) => { await deleteWechat ( ) ; await ctx . reply ( lang . login . logouted ( user . name ( ) ) ) ; } ) ; wechat ?. on ( 'error' , async ( error ) => { Logger . warn ( error . message ) ; await ctx . reply ( lang . message . error ) ; await deleteWechat ( { clean : false } ) ; await this . handleLogin ( ctx ) ; } ) ; wechat ?. on ( 'message' , ( msg ) => this . handleWechatMessage ( msg , ctx ) ) ; client . initialized = true ; if ( client . wechatId ) return ; await ctx . reply ( lang . login . request ) ; await wechat ?. start ( ) ; } protected async checkUser ( ctx , next ) { for ( let c of this . beforeCheckUserList ) { if ( ! ( await c ( ctx ) ) ) return ; } if ( ! ctx ) return next ? next ( ) : undefined ; let id = ctx . chat . id ; let user = this . clients . get ( id ) ; if ( ! user ) return ; ctx [ 'user' ] = user ; next ( ctx ) ; } protected handleLogout = async ( ctx ) => { let user = ctx [ 'user' ] as Client ; if ( ! user ) return ; try { this . clients . delete ( ctx . chat . id ) ; user . wechat ?. reset ( ) ; } catch ( error ) { } await user . wechat ?. logout ( ) . catch ( ( reason ) => Logger . error ( reason ) ) ; await user . wechat ?. stop ( ) . catch ( ( reason ) => Logger . error ( reason ) ) ; ctx . reply ( lang . login . bye ) ; } ; handleQuitRoom = async ( ctx ) => { let user = ctx [ 'user' ] as Client ; if ( ! user ) return ; let room = user . currentContact as Room ; let topic = await room [ 'topic' ] ?. ( ) ; await room [ 'quit' ] ?. ( ) ; ctx . reply ( `${ topic } 👋` ) ; } ; protected handleAcceptRoomInvitation = async ( ) => { this . lastRoomInvitation ?. accept ( ) ; this . lastRoomInvitation = null ; } ; protected handleAgreeFriendship = async ( ctx ) => { for ( let [ key , req ] of this . pendingFriends ) { await req . accept ( ) ; } this . pendingFriends . clear ( ) ; } ; protected handleDisagreeFriendship = async ( ctx ) => { this . pendingFriends . clear ( ) ; await ctx . reply ( 'OK' ) ; } ; protected handleFind = ( ctx , next ) => handleFind ( this , ctx , next ) ; protected handleLock = ( ctx ) => handleLock ( ctx ) ; protected handleUnlock = ( ctx ) => handleUnlock ( ctx ) ; protected handleMute = ( ctx ) => handleMute ( ctx ) ; protected handleUnmute = ( ctx ) => handleUnmute ( ctx ) ; protected handleCurrent = handleCurrent ; protected handleForward = handleForwardTo ; protected handleTelegramMessage = ( ctx ) => handleTelegramMessage ( ctx , { ... this . options , bot : this . botSelf } ) ; protected handleWechatMessage = ( msg , ctx ) => handleWechatMessage ( this , msg , ctx ) ; }