50_TelegramBot.pm 150 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833
  1. ##############################################################################
  2. #
  3. # 50_TelegramBot.pm
  4. #
  5. # This file is part of Fhem.
  6. #
  7. # Fhem is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Fhem is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with Fhem. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. ##############################################################################
  21. #
  22. # TelegramBot (c) Johannes Viegener / https://github.com/viegener/Telegram-fhem
  23. #
  24. # This module handles receiving and sending messages to the messaging service telegram (see https://telegram.org/)
  25. # TelegramBot is making use of the Telegrom Bot API (see https://core.telegram.org/bots and https://core.telegram.org/bots/api)
  26. # For using it with fhem an telegram BOT API key is needed! --> see https://core.telegram.org/bots/api#authorizing-your-bot
  27. #
  28. # Discussed in FHEM Forum: https://forum.fhem.de/index.php/topic,38328.0.html
  29. #
  30. # $Id: 50_TelegramBot.pm 16382 2018-03-11 13:20:55Z viegener $
  31. #
  32. ##############################################################################
  33. # 0.0 2015-09-16 Started
  34. # 1.0 2015-10-17 Initial SVN-Version
  35. # 2.0 2016-10-19 multibot support / markup on send text / msgEdit
  36. # disable_web_page_preview - attribut webPagePreview - msg506924
  37. # log other messages in getupdate
  38. # add new get command "update" for single update poll
  39. # new cmd for forcing a msg reply - msgForceReply
  40. # add readings for reply msg id: msgReplyMsgId
  41. # documemt: msgForceReply, msgReplyMsgId
  42. # diable attribute to stop polling
  43. # keyboards through [] in message command(s) -> no changed to () to avoid issues
  44. # send incomplete keyboards as message instead of error
  45. # Add | as separator for keys
  46. # documentation alignment - more consistent usage of peer (instead of user)
  47. # Keyboards in () istead of []
  48. # added callback being retrieved in updates
  49. # allow inline keyboards sent - new command inline keys titel:data
  50. # allow answer to callback (id must be given / text is optional)
  51. # document inline / answer
  52. # cleaned up recommendations for cmdKeyword etc
  53. # corrections to doc and code - msg540802
  54. # command names for answer / inline -changed to-> queryInline, queryAnswer - msg540802
  55. # attribute for automatic answer - eval set logic - queryAnswerText
  56. # FIX: trim $ret avoiding empty msg error from telegram in command response
  57. # FIX: trim $ret avoiding empty msg error from telegram also with control characters in 2 chars
  58. # rename callback... readings to query... for consistency
  59. # value 0 for queryAnswerText means no text sent but still answer
  60. # FIX: corrected documentation - unbalanced li
  61. # Run set magic on all comands before execution
  62. # add new reading sentMsgPeerId
  63. # add edit message for inline keyboards?
  64. # document queryEditInline
  65. # "0" message still sent on queryanswer
  66. # 2.1 2016-12-25 msgForceReply, disable, keyboards in messages, inline keyboards and dialogs
  67. # allow response for commands being sent in chats - new attribute cmdRespondChat to configure
  68. # new reading msgChatId
  69. # exclamation mark in favorites to allow empty results also being sent
  70. # new get peerID for onverting a named peer into an id (same syntax as in msg)
  71. # document get commands
  72. # communication with TBot_List Module -> queryAnswer
  73. # document cmdRespondChat / msgChatId
  74. # "Bad Request:" or "Unauthorized" do not result in retry
  75. # cleaned up done list
  76. # ATTENTION: store api key in setkey value see patch from msg576714
  77. # put values in chat/chatId even if no group involved (peer will be set)
  78. # 2.2 2017-02-26 msgChatId with peer / api key secured / communication with TBot_List
  79. # cmdSend to send the result of a command as message (used for sending SVGs)
  80. # add utf8Special attribute for encoding before send
  81. # reset msgReplyMsgId on reception to empty if no replyid
  82. # clarified scope of cmdRestrictedPeer in doc
  83. # changed utf8Special to downgrade
  84. # FIX: defpeer undefined in #msg605605
  85. # FIXDOC: url escaping for filenames
  86. # avoid empty favorites
  87. # allow multiple commands in favorites with double ;;
  88. # DOC: multiple commands in favorites
  89. # allow flagging favorites not shown in favlist (only with alias prefixing alias with a hyphen --> /-alias )
  90. # FIX: Allow utf8 again
  91. # alias execution is not honoring needsconfirm and sent result --> needs to be backward compatible
  92. # cleanup for favorite execution and parsing
  93. # reduce utf8 handling
  94. # add favorite hidden zusatz
  95. # favorite keyboard 2 column #msg609128
  96. # 2.3 2017-03-27 utf8Special for unicode issues / favorite handling / hidden favorites
  97. # doc: favorites2Col for 2 columns favorites keyboard
  98. # fix: aliasExec can be undefined - avoid error
  99. # allow : in keyboards (either escaped or just use last : for data split) --> #msg611609
  100. # allow space before = in favorite
  101. # favoritesInline attribute for having favorites handled with inline keyboards
  102. # INT: addtl Parameter in SendIt for options (-msgid-)
  103. # Handle favorites as inline
  104. # document favoritesInline
  105. # remove old inline favorites dialog on execution of commands
  106. # allow execution of hidden favorites from inline menu
  107. # Debug/log cleanup
  108. # 2.4 2017-05-25 favorites rework - inline / allow : in inline
  109. # fix: options remove in sendit corrected: #msg641797
  110. # DOCFIX: Double semicolon for multiple commands in favorites
  111. # FIX: non-local $_ - see #msg647071
  112. # 2.4.1 2017-06-16 minor fixes - #msg641797 / #msg647071
  113. # FIX: make fileread work for both old and new perl versions
  114. # 2.4.2 2017-07-01 rewrite read file function due to $_ warning - #msg651947
  115. # FIX: make delayed retry work again
  116. # rename of bot also works with token encryption - #msg668108
  117. # 2.4.3 2017-08-13 delayed retry & rename (#msg668108)
  118. # remove debug / addtl testing
  119. # adapt prototypes for token
  120. # additional logs / removed debugs
  121. # special httputils debug lines added
  122. # add msgDelete function to delete messages sent before from the bot
  123. # added check for msgId not given as first parameter (e.g. msgDelete / msgEdit)
  124. # 2.5 2017-09-10 new set cmd msgDelete
  125. # add - in description will not show favorite command in menu #msg686352
  126. # Issue: when direct favorite confirm is cancelled - do not jump to favorite menu
  127. # json_decode mit nonref #msg687580
  128. # 2.6 2017-09-24 hide command in favorites/change direct favorites confirm
  129. # Fix minusdesc undefined issue
  130. # Cleanup old code
  131. # add favoritesMenu to send favorites
  132. # doc favoritesMenu
  133. # correct favoritesMenu to allow parameter
  134. # FIX: allow_nonref / eval also for makekeyboard #msg732757
  135. # new set cmd silentmsg for disable_notification - syntax as in msg
  136. # INT: change forceReply to options for sendit
  137. # 2.7 2017-12-20 set command silentmsg
  138. # new set cmd silentImage for disable_notification - syntax as in sendImage
  139. # FIX: allow queryAsnwer also with defaultpeer not set #msg757339
  140. # General. set commands not requiring a peer - internally set peers to 0 for sendit
  141. # FIX: Doc missing end code tag
  142. # Change log to not write _Set/_Get on ? parameter
  143. # attr to handle set / del types for polling/allowedCmds/favorites
  144. # silentInline added
  145. # cmdSendSilent added and documented
  146. # tests and fixes on handling of peers/chats for tbot_list and replies
  147. # FIX: peer names not numeric in send commands
  148. # FIX: disable also sending messages
  149. # FIX: have disable attribute with dropdown
  150. # Allow caption in sendImage also with \n\t
  151. # 2.8 2018-03-11 more silent cmds, caption formatting, several fixes
  152. #
  153. ##############################################################################
  154. # TASKS
  155. #
  156. # additional silent commands
  157. #
  158. # queryDialogStart / queryDialogEnd - keep msg id
  159. #
  160. # remove keyboard after favorite confirm
  161. #
  162. # cleanup encodings
  163. #
  164. # replyKeyboardRemove - #msg592808
  165. #
  166. # \n in inline keyboards - not possible currently
  167. #
  168. ##############################################################################
  169. package main;
  170. use strict;
  171. use warnings;
  172. use HttpUtils;
  173. use utf8;
  174. use Encode;
  175. # JSON:XS is used here normally
  176. use JSON;
  177. use File::Basename;
  178. use URI::Escape;
  179. use Scalar::Util qw(reftype looks_like_number);
  180. #########################
  181. # Forward declaration
  182. sub TelegramBot_Define($$);
  183. sub TelegramBot_Undef($$);
  184. sub TelegramBot_Set($@);
  185. sub TelegramBot_Get($@);
  186. sub TelegramBot_Callback($$$);
  187. sub TelegramBot_SendIt($$$$$;$$$);
  188. sub TelegramBot_checkAllowedPeer($$$);
  189. sub TelegramBot_SplitFavoriteDef($$);
  190. sub TelegramBot_AttrNum($$$);
  191. sub TelegramBot_MakeKeyboard($$$@);
  192. sub TelegramBot_ExecuteCommand($$$$;$$);
  193. sub TelegramBot_readToken($;$);
  194. sub TelegramBot_storeToken($$;$);
  195. #########################
  196. # Globals
  197. my %sets = (
  198. "_msg" => "textField",
  199. "message" => "textField",
  200. "msg" => "textField",
  201. "send" => "textField",
  202. "silentmsg" => "textField",
  203. "silentImage" => "textField",
  204. "silentInline" => "textField",
  205. "msgDelete" => "textField",
  206. "msgEdit" => "textField",
  207. "msgForceReply" => "textField",
  208. "queryAnswer" => "textField",
  209. "queryInline" => "textField",
  210. "queryEditInline" => "textField",
  211. "sendImage" => "textField",
  212. "sendPhoto" => "textField",
  213. "sendDocument" => "textField",
  214. "sendMedia" => "textField",
  215. "sendVoice" => "textField",
  216. "sendLocation" => "textField",
  217. "favoritesMenu" => "textField",
  218. "cmdSend" => "textField",
  219. "cmdSendSilent" => "textField",
  220. "replaceContacts" => "textField",
  221. "reset" => undef,
  222. "reply" => "textField",
  223. "token" => "textField",
  224. "zDebug" => "textField"
  225. );
  226. my %deprecatedsets = (
  227. "image" => "textField",
  228. "sendPhoto" => "textField",
  229. );
  230. my %gets = (
  231. "urlForFile" => "textField",
  232. "update" => undef,
  233. "peerId" => "textField",
  234. );
  235. my $TelegramBot_header = "agent: TelegramBot/1.0\r\nUser-Agent: TelegramBot/1.0\r\nAccept: application/json\r\nAccept-Charset: utf-8";
  236. my $TelegramBot_arg_retrycnt = 6;
  237. ##############################################################################
  238. ##############################################################################
  239. ##
  240. ## Module operation
  241. ##
  242. ##############################################################################
  243. ##############################################################################
  244. #####################################
  245. # Initialize is called from fhem.pl after loading the module
  246. # define functions and attributed for the module and corresponding devices
  247. sub TelegramBot_Initialize($) {
  248. my ($hash) = @_;
  249. require "$attr{global}{modpath}/FHEM/DevIo.pm";
  250. $hash->{DefFn} = "TelegramBot_Define";
  251. $hash->{UndefFn} = "TelegramBot_Undef";
  252. $hash->{StateFn} = "TelegramBot_State";
  253. $hash->{GetFn} = "TelegramBot_Get";
  254. $hash->{RenameFn} = "TelegramBot_Rename";
  255. $hash->{SetFn} = "TelegramBot_Set";
  256. $hash->{AttrFn} = "TelegramBot_Attr";
  257. $hash->{AttrList} = "defaultPeer defaultPeerCopy:0,1 cmdKeyword cmdSentCommands favorites:textField-long favoritesInline:0,1 cmdFavorites cmdRestrictedPeer ". "cmdTriggerOnly:0,1 saveStateOnContactChange:1,0 maxFileSize maxReturnSize cmdReturnEmptyResult:1,0 pollingVerbose:1_Digest,2_Log,0_None ".
  258. "cmdTimeout pollingTimeout disable:1,0 queryAnswerText:textField cmdRespondChat:0,1 ".
  259. "allowUnknownContacts:1,0 textResponseConfirm:textField textResponseCommands:textField allowedCommands filenameUrlEscape:1,0 ".
  260. "textResponseFavorites:textField textResponseResult:textField textResponseUnauthorized:textField ".
  261. "parseModeSend:0_None,1_Markdown,2_HTML,3_InMsg webPagePreview:1,0 utf8Special:1,0 favorites2Col:0,1 ".
  262. " maxRetries:0,1,2,3,4,5 ".$readingFnAttributes;
  263. }
  264. ######################################
  265. # Define function is called for actually defining a device of the corresponding module
  266. # For TelegramBot this is mainly API id for the bot
  267. # data will be stored in the hash of the device as internals
  268. #
  269. sub TelegramBot_Define($$) {
  270. my ($hash, $def) = @_;
  271. my @a = split("[ \t]+", $def);
  272. my $name = $hash->{NAME};
  273. Log3 $name, 3, "TelegramBot_Define $name: called ";
  274. my $errmsg = '';
  275. # Check parameter(s)
  276. # If api token is given check for syntax and remove from hash
  277. if ( ( int(@a) == 3 ) && ( $a[2] !~ /^([[:alnum:]]|[-:_])+[[:alnum:]]+([[:alnum:]]|[-:_])+$/ ) ) {
  278. $errmsg = "specify valid API token containing only alphanumeric characters and -: characters: define <name> TelegramBot [ <APItoken> ]";
  279. Log3 $name, 1, "TelegramBot $name: " . $errmsg;
  280. return $errmsg;
  281. } elsif ( ( int(@a) == 2 ) && ( ! TelegramBot_readToken($hash) ) ){
  282. $errmsg = "no predefined token found specify token in define: define <name> TelegramBot <APItoken>";
  283. Log3 $name, 1, "TelegramBot $name: " . $errmsg;
  284. return $errmsg;
  285. } elsif( int(@a) > 3 || int(@a) < 2) {
  286. $errmsg = "syntax error: define <name> TelegramBot [ <APIid> ]";
  287. Log3 $name, 1, "TelegramBot $name: " . $errmsg;
  288. return $errmsg;
  289. }
  290. my $ret;
  291. $hash->{TYPE} = "TelegramBot";
  292. $hash->{STATE} = "Undefined";
  293. $hash->{WAIT} = 0;
  294. $hash->{FAILS} = 0;
  295. $hash->{UPDATER} = 0;
  296. $hash->{POLLING} = -1;
  297. my %hu_upd_params = (
  298. url => "",
  299. timeout => 5,
  300. method => "GET",
  301. header => $TelegramBot_header,
  302. isPolling => "update",
  303. hideurl => 1,
  304. callback => \&TelegramBot_Callback
  305. );
  306. my %hu_do_params = (
  307. url => "",
  308. timeout => 30,
  309. method => "GET",
  310. header => $TelegramBot_header,
  311. hideurl => 1,
  312. callback => \&TelegramBot_Callback
  313. );
  314. $hash->{HU_UPD_PARAMS} = \%hu_upd_params;
  315. $hash->{HU_DO_PARAMS} = \%hu_do_params;
  316. if (int(@a) == 3) {
  317. TelegramBot_storeToken($hash, $a[2]);
  318. $hash->{DEF} = undef;
  319. }
  320. TelegramBot_Setup( $hash );
  321. return $ret;
  322. }
  323. #####################################
  324. # Undef function is corresponding to the delete command the opposite to the define function
  325. # Cleanup the device specifically for external ressources like connections, open files,
  326. # external memory outside of hash, sub processes and timers
  327. sub TelegramBot_Undef($$)
  328. {
  329. my ($hash, $arg) = @_;
  330. my $name = $hash->{NAME};
  331. Log3 $name, 3, "TelegramBot_Undef $name: called ";
  332. HttpUtils_Close($hash->{HU_UPD_PARAMS});
  333. HttpUtils_Close($hash->{HU_DO_PARAMS});
  334. RemoveInternalTimer($hash);
  335. RemoveInternalTimer($hash->{HU_DO_PARAMS});
  336. Log3 $name, 4, "TelegramBot_Undef $name: done ";
  337. return undef;
  338. }
  339. #############################################################################################
  340. # called when the device gets renamed,
  341. # in this case we then also need to rename the key in the token store and ensure it is recoded with new name
  342. sub TelegramBot_Rename($$) {
  343. my ($new,$old) = @_;
  344. my $nhash = $defs{$new};
  345. my $token = TelegramBot_readToken( $nhash, $old );
  346. TelegramBot_storeToken( $nhash, $token );
  347. # remove old token with old name
  348. my $index_old = "TelegramBot_" . $old . "_token";
  349. setKeyValue($index_old, undef);
  350. }
  351. ##############################################################################
  352. ##############################################################################
  353. ##
  354. ## Instance operational methods
  355. ##
  356. ##############################################################################
  357. ##############################################################################
  358. ####################################
  359. # State function to ensure contacts internal hash being reset on Contacts Readings Set
  360. sub TelegramBot_State($$$$) {
  361. my ($hash, $time, $name, $value) = @_;
  362. # Log3 $hash->{NAME}, 4, "TelegramBot_State called with :$name: value :$value:";
  363. if ($name eq 'Contacts') {
  364. TelegramBot_CalcContactsHash( $hash, $value );
  365. Log3 $hash->{NAME}, 4, "TelegramBot_State Contacts hash has now :".scalar(keys %{$hash->{Contacts}}).":";
  366. }
  367. return undef;
  368. }
  369. ####################################
  370. # set function for executing set operations on device
  371. sub TelegramBot_Set($@)
  372. {
  373. my ( $hash, $name, @args ) = @_;
  374. Log3 $name, 5, "TelegramBot_Set $name: called ";
  375. ### Check Args
  376. my $numberOfArgs = int(@args);
  377. return "TelegramBot_Set: No cmd specified for set" if ( $numberOfArgs < 1 );
  378. my $cmd = shift @args;
  379. if (!exists($sets{$cmd})) {
  380. my @cList;
  381. foreach my $k (keys %sets) {
  382. my $opts = undef;
  383. $opts = $sets{$k};
  384. if (defined($opts)) {
  385. push(@cList,$k . ':' . $opts);
  386. } else {
  387. push (@cList,$k);
  388. }
  389. } # end foreach
  390. return "TelegramBot_Set: Unknown argument $cmd, choose one of " . join(" ", @cList);
  391. } # error unknown cmd handling
  392. Log3 $name, 4, "TelegramBot_Set $name: Processing TelegramBot_Set( $cmd )";
  393. my $ret = undef;
  394. if( ($cmd eq 'message') || ($cmd eq 'queryInline') || ($cmd eq 'queryEditInline') || ($cmd eq 'queryAnswer') ||
  395. ($cmd eq 'msg') || ($cmd eq '_msg') || ($cmd eq 'reply') || ($cmd eq 'msgEdit') || ($cmd eq 'msgForceReply') ||
  396. ($cmd eq 'silentmsg') || ($cmd eq 'silentImage') || ($cmd eq 'silentInline') || ($cmd =~ /^send.*/ ) ) {
  397. my $msgid;
  398. my $msg;
  399. my $addPar;
  400. my $sendType = 0;
  401. my $options = "";
  402. my $peers;
  403. my $inline = 0;
  404. my $needspeer = 1;
  405. if ( ($cmd eq 'reply') || ($cmd eq 'msgEdit' ) || ($cmd eq 'queryEditInline' ) ) {
  406. return "TelegramBot_Set: Command $cmd, no msgid and no text/file specified" if ( $numberOfArgs < 3 );
  407. $msgid = shift @args;
  408. return "TelegramBot_Set: Command $cmd, msgId must be given as first parameter before peer" if ( $msgid =~ /^@/ );
  409. $numberOfArgs--;
  410. # all three messages need also a peer/chat_id
  411. } elsif ($cmd eq 'queryAnswer') {
  412. $needspeer = 0;
  413. }
  414. # special options
  415. $inline = 1 if ( ($cmd eq 'queryInline') || ($cmd eq 'queryEditInline') || ($cmd eq 'silentInline') );
  416. $options .= " -force_reply- " if ($cmd eq 'msgForceReply');
  417. $options .= " -silent- " if ( ($cmd eq 'silentmsg') || ($cmd eq 'silentImage') || ($cmd eq 'silentInline') ) ;
  418. return "TelegramBot_Set: Command $cmd, no peers and no text/file specified" if ( $numberOfArgs < 2 );
  419. # numberOfArgs might not be correct beyond this point
  420. while ( $args[0] =~ /^@(..+)$/ ) {
  421. my $ppart = $1;
  422. return "TelegramBot_Set: Command $cmd, need exactly one peer" if ( ($cmd eq 'reply') && ( defined( $peers ) ) );
  423. $peers .= " " if ( defined( $peers ) );
  424. $peers = "" if ( ! defined( $peers ) );
  425. $peers .= $ppart;
  426. shift @args;
  427. last if ( int(@args) == 0 );
  428. }
  429. return "TelegramBot_Set: Command $cmd, no msg content specified" if ( int(@args) < 1 );
  430. if ( ($needspeer ) && ( ! defined( $peers ) ) ) {
  431. $peers = AttrVal($name,'defaultPeer',undef);
  432. return "TelegramBot_Set: Command $cmd, without explicit peer requires defaultPeer being set" if ( ! defined($peers) );
  433. } elsif ( ! defined( $peers ) ) {
  434. $peers = 0;
  435. }
  436. if ( ($cmd eq 'sendPhoto') || ($cmd eq 'sendImage') || ($cmd eq 'image') || ($cmd eq 'silentImage') ) {
  437. $sendType = 1;
  438. } elsif ($cmd eq 'sendVoice') {
  439. $sendType = 2;
  440. } elsif ( ($cmd eq 'sendDocument') || ($cmd eq 'sendMedia') ) {
  441. $sendType = 3;
  442. } elsif ( ($cmd eq 'msgEdit') || ($cmd eq 'queryEditInline') ) {
  443. $sendType = 10;
  444. } elsif ($cmd eq 'sendLocation') {
  445. $sendType = 11;
  446. } elsif ($cmd eq 'queryAnswer') {
  447. $sendType = 12;
  448. }
  449. if ( $sendType == 11 ) {
  450. # location
  451. return "TelegramBot_Set: Command $cmd, 2 parameters latitude / longitude need to be specified" if ( int(@args) != 2 );
  452. # first latitude
  453. $msg = shift @args;
  454. # first longitude
  455. $addPar = shift @args;
  456. } elsif ( $sendType == 12 ) {
  457. # inline query
  458. return "TelegramBot_Set: Command $cmd, no inline query id given" if ( int(@args) < 1 );
  459. # first inline query id
  460. $addPar = shift @args;
  461. # remaining msg
  462. $msg = "";
  463. $msg = join(" ", @args ) if ( int(@args) > 0 );
  464. } elsif ( ( $sendType > 0 ) && ( $sendType < 10 ) ) {
  465. # should return undef if succesful
  466. $msg = shift @args;
  467. $msg = $1 if ( $msg =~ /^\"(.*)\"$/ );
  468. if ( $sendType == 1 ) {
  469. # for Photos a caption can be given
  470. $addPar = join(" ", @args ) if ( int(@args) > 0 );
  471. } else {
  472. return "TelegramBot_Set: Command $cmd, extra parameter specified after filename" if ( int(@args) > 0 );
  473. }
  474. } else {
  475. if ( ! defined( $addPar ) ) {
  476. # check for Keyboard given (only if not forcing reply) and parse it to keys / jsonkb
  477. my @keys;
  478. while ( $args[0] =~ /^\s*\(.*$/ ) {
  479. my $aKey = "";
  480. while ( $aKey !~ /^\s*\((.*)\)\s*$/ ) {
  481. $aKey .= " ".$args[0];
  482. shift @args;
  483. last if ( int(@args) == 0 );
  484. }
  485. # trim key
  486. $aKey =~ s/^\s+|\s+$//g;
  487. if ( $aKey =~ /^\((.*)\)$/ ) {
  488. my @tmparr = split( /\|/, $1 );
  489. push( @keys, \@tmparr );
  490. } else {
  491. # incomplete key handle as message
  492. unshift( @args, $aKey ) if ( length( $aKey ) > 0 );
  493. last;
  494. }
  495. }
  496. $addPar = TelegramBot_MakeKeyboard( $hash, 1, $inline, @keys ) if ( scalar( @keys ) );
  497. }
  498. return "TelegramBot_Set: Command $cmd, no text for msg specified " if ( int(@args) == 0 );
  499. $msg = join(" ", @args );
  500. }
  501. Log3 $name, 5, "TelegramBot_Set $name: start send for cmd :$cmd: and sendType :$sendType:";
  502. $ret = TelegramBot_SendIt( $hash, $peers, $msg, $addPar, $sendType, $msgid, $options );
  503. } elsif($cmd eq 'favoritesMenu') {
  504. my $peers;
  505. if ( int(@args) > 0 ) {
  506. while ( $args[0] =~ /^@(..+)$/ ) {
  507. my $ppart = $1;
  508. return "TelegramBot_Set: Command $cmd, need exactly one peer" if ( ( defined( $peers ) ) );
  509. $peers = (defined($peers)?$peers." ":"").$ppart;
  510. shift @args;
  511. last if ( int(@args) == 0 );
  512. }
  513. return "TelegramBot_Set: Command $cmd, addiitonal parameter specified" if ( int(@args) >= 1 );
  514. }
  515. if ( ! defined( $peers ) ) {
  516. $peers = AttrVal($name,'defaultPeer',undef);
  517. return "TelegramBot_Set: Command $cmd, without explicit peer requires defaultPeer being set" if ( ! defined($peers) );
  518. }
  519. return "TelegramBot_Set: Command $cmd, no favorites defined" if ( ! defined( AttrVal($name,'favorites',undef) ) );
  520. TelegramBot_SendFavorites($hash, $peers, undef, "", undef, undef, 0);
  521. } elsif($cmd =~ 'cmdSend(Silent)?') {
  522. return "TelegramBot_Set: Command $cmd, no peers and no text/file specified" if ( $numberOfArgs < 2 );
  523. # numberOfArgs might not be correct beyond this point
  524. my $options = "";
  525. $options .= " -silent- " if ( ($cmd eq 'cmdSendSilent') ) ;
  526. my $peers;
  527. while ( $args[0] =~ /^@(..+)$/ ) {
  528. my $ppart = $1;
  529. return "TelegramBot_Set: Command $cmd, need exactly one peer" if ( ( defined( $peers ) ) );
  530. $peers .= " " if ( defined( $peers ) );
  531. $peers = "" if ( ! defined( $peers ) );
  532. $peers .= $ppart;
  533. shift @args;
  534. last if ( int(@args) == 0 );
  535. }
  536. return "TelegramBot_Set: Command $cmd, no msg content specified" if ( int(@args) < 1 );
  537. if ( ! defined( $peers ) ) {
  538. $peers = AttrVal($name,'defaultPeer',undef);
  539. return "TelegramBot_Set: Command $cmd, without explicit peer requires defaultPeer being set" if ( ! defined($peers) );
  540. }
  541. # Execute command
  542. my $isMediaStream = 0;
  543. my $msg;
  544. my $scmd = join(" ", @args );
  545. # run replace set magic on command - first
  546. my %dummy;
  547. my ($err, @a) = ReplaceSetMagic(\%dummy, 0, ( $scmd ) );
  548. if ( $err ) {
  549. Log3 $name, 1, "TelegramBot_Set $name: parse cmd failed on ReplaceSetmagic with :$err: on :$scmd:";
  550. } else {
  551. $msg = join(" ", @a);
  552. Log3 $name, 4, "TelegramBot_Set $name: parse cmd returned :$msg:";
  553. }
  554. $msg = AnalyzeCommandChain( $hash, $msg );
  555. # Check for image/doc/audio stream in return (-1 image
  556. ( $isMediaStream ) = TelegramBot_IdentifyStream( $hash, $msg ) if ( defined( $msg ) );
  557. Log3 $name, 5, "TelegramBot_Set $name: start send for cmd :$cmd: and isMediaStream :$isMediaStream:";
  558. $ret = TelegramBot_SendIt( $hash, $peers, $msg, undef, $isMediaStream, undef, $options );
  559. } elsif($cmd eq 'msgDelete') {
  560. my $peers;
  561. my $sendType = 20;
  562. return "TelegramBot_Set: Command $cmd, no peer and no msgid specified" if ( $numberOfArgs < 2 );
  563. my $msgid = shift @args;
  564. return "TelegramBot_Set: Command $cmd, msgId must be given as first parameter before peer" if ( $msgid =~ /^@/ );
  565. $numberOfArgs--;
  566. while ( $args[0] =~ /^@(..+)$/ ) {
  567. my $ppart = $1;
  568. return "TelegramBot_Set: Command $cmd, need exactly one peer" if ( defined( $peers ) );
  569. $peers .= " " if ( defined( $peers ) );
  570. $peers = "" if ( ! defined( $peers ) );
  571. $peers .= $ppart;
  572. shift @args;
  573. last if ( int(@args) == 0 );
  574. }
  575. if ( ! defined( $peers ) ) {
  576. $peers = AttrVal($name,'defaultPeer',undef);
  577. return "TelegramBot_Set: Command $cmd, without explicit peer requires defaultPeer being set" if ( ! defined($peers) );
  578. }
  579. Log3 $name, 5, "TelegramBot_Set $name: start send for cmd :$cmd: and sendType :$sendType:";
  580. $ret = TelegramBot_SendIt( $hash, $peers, "", undef, $sendType, $msgid );
  581. } elsif($cmd eq 'zDebug') {
  582. # for internal testing only
  583. Log3 $name, 5, "TelegramBot_Set $name: start debug option ";
  584. # delete $hash->{sentMsgPeer};
  585. # $ret = TelegramBot_SendIt( $hash, AttrVal($name,'defaultPeer',undef), "abc def\n def ghi", undef, 0, undef );
  586. $hash->{HU_UPD_PARAMS}->{callback} = \&TelegramBot_Callback;
  587. $hash->{HU_DO_PARAMS}->{callback} = \&TelegramBot_Callback;
  588. } elsif($cmd eq 'token') {
  589. if ( $numberOfArgs == 2 ) {
  590. $ret = TelegramBot_storeToken ( $hash, $args[0] );
  591. TelegramBot_Setup( $hash );
  592. } else {
  593. return "TelegramBot_Set: Command $cmd no token specified or addtl parameters given";
  594. }
  595. } elsif($cmd eq 'reset') {
  596. Log3 $name, 5, "TelegramBot_Set $name: reset requested ";
  597. TelegramBot_Setup( $hash );
  598. } elsif($cmd eq 'replaceContacts') {
  599. if ( $numberOfArgs < 2 ) {
  600. return "TelegramBot_Set: Command $cmd, need to specify contacts string separate by space and contacts in the form of <id>:<full_name>:[@<username>|#<groupname>] ";
  601. }
  602. my $arg = join(" ", @args );
  603. Log3 $name, 3, "TelegramBot_Set $name: set new contacts to :$arg: ";
  604. # first set the hash accordingly
  605. TelegramBot_CalcContactsHash($hash, $arg);
  606. # then calculate correct string reading and put this into the reading
  607. my @dumarr;
  608. TelegramBot_ContactUpdate($hash, @dumarr);
  609. Log3 $name, 5, "TelegramBot_Set $name: contacts newly set ";
  610. }
  611. if ( ! defined( $ret ) ) {
  612. Log3 $name, 5, "TelegramBot_Set $name: $cmd done succesful: ";
  613. } else {
  614. Log3 $name, 5, "TelegramBot_Set $name: $cmd failed with :$ret: ";
  615. }
  616. return $ret
  617. }
  618. #####################################
  619. # get function for gaining information from device
  620. sub TelegramBot_Get($@)
  621. {
  622. my ( $hash, $name, @args ) = @_;
  623. Log3 $name, 5, "TelegramBot_Get $name: called ";
  624. ### Check Args
  625. my $numberOfArgs = int(@args);
  626. return "TelegramBot_Get: No value specified for get" if ( $numberOfArgs < 1 );
  627. my $cmd = $args[0];
  628. my $arg = ($args[1] ? $args[1] : "");
  629. if(!exists($gets{$cmd})) {
  630. my @cList;
  631. foreach my $k (sort keys %gets) {
  632. my $opts = undef;
  633. $opts = $sets{$k};
  634. if (defined($opts)) {
  635. push(@cList,$k . ':' . $opts);
  636. } else {
  637. push (@cList,$k);
  638. }
  639. } # end foreach
  640. return "TelegramBot_Get: Unknown argument $cmd, choose one of " . join(" ", @cList);
  641. } # error unknown cmd handling
  642. Log3 $name, 4, "TelegramBot_Get $name: Processing TelegramBot_Get( $cmd )";
  643. my $ret = undef;
  644. if($cmd eq 'urlForFile') {
  645. if ( $numberOfArgs != 2 ) {
  646. return "TelegramBot_Get: Command $cmd, no file id specified";
  647. }
  648. $hash->{fileUrl} = "";
  649. # return URL for file id
  650. my $url = TelegramBot_getBaseURL($hash)."getFile?file_id=".urlEncode($arg);
  651. my $guret = TelegramBot_DoUrlCommand( $hash, $url );
  652. my $token = TelegramBot_readToken( $hash );
  653. if ( ( defined($guret) ) && ( ref($guret) eq "HASH" ) ) {
  654. if ( defined($guret->{file_path} ) ) {
  655. # URL is https://api.telegram.org/file/bot<token>/<file_path>
  656. my $filePath = $guret->{file_path};
  657. $hash->{fileUrl} = "https://api.telegram.org/file/bot".$token."/".$filePath;
  658. $ret = $hash->{fileUrl};
  659. } else {
  660. $ret = "urlForFile failed: no file path found";
  661. $hash->{fileUrl} = $ret;
  662. }
  663. } else {
  664. $ret = "urlForFile failed: ".(defined($guret)?$guret:"<undef>");
  665. $hash->{fileUrl} = $ret;
  666. }
  667. } elsif ( $cmd eq "update" ) {
  668. $ret = TelegramBot_UpdatePoll( $hash, "doOnce" );
  669. } elsif ( $cmd eq "peerId" ) {
  670. if ( $numberOfArgs != 2 ) {
  671. return "TelegramBot_Get: Command $cmd, peer specified";
  672. }
  673. $ret = TelegramBot_GetIdForPeer( $hash, $arg );
  674. }
  675. Log3 $name, 5, "TelegramBot_Get $name: done with ".( defined($ret)?$ret:"<undef>").": ";
  676. return $ret
  677. }
  678. ##############################
  679. # attr function for setting fhem attributes for the device
  680. sub TelegramBot_Attr(@) {
  681. my ($cmd,$name,$aName,$aVal) = @_;
  682. my $hash = $defs{$name};
  683. Log3 $name, 5, "TelegramBot_Attr $name: called ";
  684. return "\"TelegramBot_Attr: \" $name does not exist" if (!defined($hash));
  685. if (defined($aVal)) {
  686. Log3 $name, 5, "TelegramBot_Attr $name: $cmd on $aName to $aVal";
  687. } else {
  688. Log3 $name, 5, "TelegramBot_Attr $name: $cmd on $aName to <undef>";
  689. }
  690. # $cmd can be "del" or "set"
  691. # $name is device name
  692. # aName and aVal are Attribute name and value
  693. if ($aName eq 'favorites') {
  694. # Empty current alias list in hash
  695. if ( defined( $hash->{AliasCmds} ) ) {
  696. foreach my $key (keys %{$hash->{AliasCmds}} )
  697. {
  698. delete $hash->{AliasCmds}{$key};
  699. }
  700. } else {
  701. $hash->{AliasCmds} = {};
  702. }
  703. if ($cmd eq "set") {
  704. # keep double ; for inside commands
  705. $aVal =~ s/;;/SeMiCoLoN/g;
  706. my @clist = split( /;/, $aVal);
  707. my $newVal = "";
  708. my $cnt = 0;
  709. foreach my $cs ( @clist ) {
  710. $cs =~ s/SeMiCoLoN/;;/g; # reestablish double ; for inside commands
  711. my ( $alias, $desc, $minusdesc, $parsecmd, $needsConfirm, $needsResult, $hidden ) = TelegramBot_SplitFavoriteDef( $hash, $cs );
  712. # Debug "parsecmd :".$parsecmd.": ".length($parsecmd);
  713. next if ( length($parsecmd) == 0 ); # skip emtpy commands
  714. $cnt += 1;
  715. $newVal .= ";" if ( length($newVal)>0 );
  716. $newVal .= $cs;
  717. if ( $alias ) {
  718. my $alx = $alias;
  719. my $alcmd = $parsecmd;
  720. Log3 $name, 2, "TelegramBot_Attr $name: Alias $alcmd defined multiple times" if ( defined( $hash->{AliasCmds}{$alx} ) );
  721. $hash->{AliasCmds}{$alx} = $cnt;
  722. }
  723. }
  724. # set attribute value to newly combined commands
  725. $attr{$name}{'favorites'} = $newVal;
  726. $aVal = $newVal;
  727. }
  728. } elsif ($aName eq 'allowedCommands') {
  729. my $allowedName = "allowed_$name";
  730. my $exists = ($defs{$allowedName} ? 1 : 0);
  731. my $alcmd = (($cmd eq "set")?$aVal:"<none>");
  732. AnalyzeCommand(undef, "defmod $allowedName allowed");
  733. AnalyzeCommand(undef, "attr $allowedName validFor $name");
  734. AnalyzeCommand(undef, "attr $allowedName $aName ".$alcmd);
  735. Log3 $name, 3, "TelegramBot_Attr $name: ".($exists ? "modified":"created")." $allowedName with commands :$alcmd:";
  736. # allowedCommands only set on the corresponding allowed_device
  737. return "\"TelegramBot_Attr: \" $aName ".($exists ? "modified":"created")." $allowedName with commands :$alcmd:"
  738. } elsif ($aName eq 'pollingTimeout') {
  739. return "\"TelegramBot_Attr: \" $aName needs to be given in digits only" if ( ($cmd eq "set") && ( $aVal !~ /^[[:digit:]]+$/ ) );
  740. # let all existing methods run into block
  741. RemoveInternalTimer($hash);
  742. $hash->{POLLING} = -1;
  743. # wait some time before next polling is starting
  744. TelegramBot_ResetPolling( $hash );
  745. } elsif ($aName eq 'disable') {
  746. return "\"TelegramBot_Attr: \" $aName needs to be 1 or 0" if ( ($cmd eq "set") && ( $aVal !~ /^(1|0)$/ ) );
  747. # let all existing methods run into block
  748. RemoveInternalTimer($hash);
  749. $hash->{POLLING} = -1;
  750. # wait some time before next polling is starting
  751. TelegramBot_ResetPolling( $hash );
  752. # attributes where only the set is relevant for syntax check
  753. } elsif ($cmd eq "set") {
  754. if ($aName eq 'cmdRestrictedPeer') {
  755. $aVal =~ s/^\s+|\s+$//g;
  756. } elsif ( ($aName eq 'defaultPeerCopy') ||
  757. ($aName eq 'saveStateOnContactChange') ||
  758. ($aName eq 'cmdReturnEmptyResult') ||
  759. ($aName eq 'cmdTriggerOnly') ||
  760. ($aName eq 'allowUnknownContacts') ) {
  761. $aVal = ($aVal eq "1")? "1": "0";
  762. } elsif ( ($aName eq 'maxFileSize') ||
  763. ($aName eq 'maxReturnSize') ||
  764. ($aName eq 'maxRetries') ) {
  765. return "\"TelegramBot_Attr: \" $aName needs to be given in digits only" if ( $aVal !~ /^[[:digit:]]+$/ );
  766. } elsif ($aName eq 'pollingVerbose') {
  767. return "\"TelegramBot_Attr: \" Incorrect value given for pollingVerbose" if ( $aVal !~ /^((1_Digest)|(2_Log)|(0_None))$/ );
  768. }
  769. $_[3] = $aVal;
  770. }
  771. return undef;
  772. }
  773. ##############################################################################
  774. ##############################################################################
  775. ##
  776. ## Command handling
  777. ##
  778. ##############################################################################
  779. ##############################################################################
  780. #####################################
  781. #####################################
  782. # INTERNAL: Check against cmdkeyword given (no auth check !!!!)
  783. sub TelegramBot_checkCmdKeyword($$$$$$) {
  784. my ($hash, $mpeernorm, $mchatnorm, $mtext, $cmdKey, $needsSep ) = @_;
  785. my $name = $hash->{NAME};
  786. my $cmd;
  787. my $doRet = 0;
  788. # Log3 $name, 3, "TelegramBot_checkCmdKeyword $name: check :".$mtext.": against defined :".$ck.": results in ".index($mtext,$ck);
  789. return ( undef, 0 ) if ( ! defined( $cmdKey ) );
  790. # Trim and then if requested add a space to the cmdKeyword
  791. $cmdKey =~ s/^\s+|\s+$//g;
  792. my $ck = $cmdKey;
  793. # Check special case end of messages considered separator
  794. if ( $mtext ne $ck ) {
  795. $ck .= " " if ( $needsSep );
  796. return ( undef, 0 ) if ( index($mtext,$ck) != 0 );
  797. }
  798. $cmd = substr( $mtext, length($ck) );
  799. $cmd =~ s/^\s+|\s+$//g;
  800. # validate security criteria for commands and return cmd only if succesful
  801. return ( undef, 1 ) if ( ! TelegramBot_checkAllowedPeer( $hash, $mpeernorm, $mtext ) );
  802. return ( undef, 1 ) if ( ( $mchatnorm ) && ( ! TelegramBot_checkAllowedPeer( $hash, $mchatnorm, $mtext ) ) );
  803. return ( $cmd, 1 );
  804. }
  805. #####################################
  806. #####################################
  807. # INTERNAL: Split Favorite def in alias(optional), description (optional), parsecmd, needsConfirm
  808. sub TelegramBot_SplitFavoriteDef($$) {
  809. my ($hash, $cmd ) = @_;
  810. my $name = $hash->{NAME};
  811. # Valid favoritedef
  812. # list TYPE=SOMFY
  813. # ?set TYPE=CUL_WM getconfig
  814. # /rolladen=list TYPE=SOMFY
  815. # /rolladen=?list TYPE=SOMFY
  816. # /-rolladen=list TYPE=SOMFY
  817. # /[Liste Rolladen]=list TYPE=SOMFY
  818. # /[Liste Rolladen]=?list TYPE=SOMFY
  819. # /rolladen[Liste Rolladen]=list TYPE=SOMFY
  820. # /-rolladen[Liste Rolladen]=list TYPE=SOMFY
  821. # /rolladen[Liste Rolladen]=list TYPE=SOMFY
  822. # /[-Liste Rolladen]=list TYPE=SOMFY
  823. # /[-Liste Rolladen]=?list TYPE=SOMFY
  824. # /rolladen[-Liste Rolladen]=list TYPE=SOMFY
  825. # /-rolladen[-Liste Rolladen]=list TYPE=SOMFY
  826. # /rolladen[-Liste Rolladen]=list TYPE=SOMFY
  827. my ( $alias, $desc, $parsecmd, $confirm, $result, $hidden, $minusdesc );
  828. $confirm = "";
  829. $result = "";
  830. $hidden = 0;
  831. $minusdesc = "";
  832. if ( $cmd =~ /^\s*((\/([^\[=]*)?)(\[(-)?([^\]]+)\])?\s*=)?(\??)(\!?)(.*?)$/ ) {
  833. $alias = $2;
  834. $minusdesc = $5 if ( $5 );
  835. $desc = $6;
  836. $confirm = $7 if ( $7 );
  837. $result = $8 if ( $8 );
  838. $parsecmd = $9;
  839. $alias = undef if ( $alias && ( $alias =~ /^\/-?$/ ) );
  840. if ( $alias && ( $alias =~ /\/-/ ) ) {
  841. $hidden = 1;
  842. $alias =~ s/^\/-/\//; # remove - in alias
  843. }
  844. # replace double semicolon
  845. $parsecmd =~ s/;;/;/g if ( $parsecmd ); # reestablish double ; for inside commands
  846. # Debug "Parse 1 a:".$alias.": d:".$desc.": c:".$parsecmd.":";
  847. } else {
  848. Log3 $name, 1, "TelegramBot_SplitFavoriteDef invalid favorite definition :$cmd: ";
  849. }
  850. Log3 $name, 4, "TelegramBot_SplitFavoriteDef cmd :$cmd: \n alias :".($alias?$alias:"<undef>").": desc :".($desc?$desc:"<undef>").
  851. ": parsecmd :".($parsecmd?$parsecmd:"<undef>").
  852. ": confirm: ".(($confirm eq "?")?1:0).": result: ".(($result eq "!")?1:0).": hidden: ".$hidden;
  853. return ( $alias, $desc, (($minusdesc eq "-")?1:0), $parsecmd, (($confirm eq "?")?1:0), (($result eq "!")?1:0), $hidden );
  854. }
  855. #####################################
  856. #####################################
  857. # INTERNAL: handle favorites and alias execution
  858. # alias exec is used by case an alias command is entered
  859. #
  860. # cmd is everything after the key word
  861. # cases
  862. # empty --> list of favorites
  863. # [0-9]+ --> id no addition
  864. # [0-9]+ <addition> --> id no addition
  865. # [0-9]+ = <description or cmd> --> id automaticall no addition
  866. #
  867. # -[0-9]+- --> confirmed manually no addition
  868. # -[0-9]+- ; <addition> --> confirmed manually with addition
  869. #
  870. # -[0-9]+- = <description or cmd> --> confirmed automatically without addition
  871. # -[0-9]+- = <description or cmd> =; <addition> --> confirmed automatically with addition
  872. #
  873. #
  874. sub TelegramBot_SendFavorites($$$$$;$$) {
  875. my ($hash, $mpeernorm, $mchatnorm, $cmd, $mid, $aliasExec, $iscallback ) = @_;
  876. my $name = $hash->{NAME};
  877. $aliasExec = 0 if ( ! $aliasExec );
  878. $iscallback = 0 if ( ! $iscallback );
  879. my $ret;
  880. Log3 $name, 4, "TelegramBot_SendFavorites cmd correct peer ";
  881. my $isInline = ( AttrVal($name, "favoritesInline", 0 ) ? 1 : 0 );
  882. my $slc = AttrVal($name,'favorites',"");
  883. # keep double ; for inside commands
  884. $slc =~ s/;;/SeMiCoLoN/g;
  885. my @clist = split( /;/, $slc);
  886. my $isConfirm;
  887. my $cmdFavId;
  888. my $cmdAddition;
  889. my $resppeer = $mpeernorm;
  890. $resppeer .= "(".$mchatnorm.")" if ( $mchatnorm );
  891. my $storedMgsId = $mid if ( $isInline ); # nur queryeditinline wenn wirklich inline gesetzt
  892. Log3 $name, 4, "TelegramBot_SendFavorites cmd :$cmd: peer :$mpeernorm: aliasExec :".$aliasExec;
  893. if ( $cmd =~ /^\s*cancel\s*$/ ) {
  894. if ( $storedMgsId ) {
  895. # 10 for edit inline
  896. $ret = TelegramBot_SendIt( $hash, (($mchatnorm)?$mchatnorm:$mpeernorm), "Favoriten beendet", undef, 10, $storedMgsId );
  897. }
  898. return $ret;
  899. }
  900. if ( $cmd =~ /^\s*([0-9]+)( = .+)?\s*$/ ) {
  901. # [0-9]+ --> id no addition
  902. # [0-9]+ = <description or cmd> --> id automaticall no addition
  903. $cmdFavId = $1;
  904. } elsif ( $cmd =~ /^\s*([0-9]+)(.+)\s*$/ ) {
  905. # [0-9]+<addition> --> id addition
  906. $cmdFavId = $1;
  907. $cmdAddition = $2;
  908. } elsif ( $cmd =~ /^\s*([0-9]+)( ; (.*))?\s*$/ ) {
  909. # [0-9]+ --> id no addition
  910. # [0-9]+ ; <addition> --> id but addition
  911. $cmdFavId = $1;
  912. $cmdAddition = $3;
  913. } elsif ( $cmd =~ /^\s*-([0-9]+)-( ; (.*))?\s*$/ ) {
  914. # -[0-9]+- --> confirmed manually no addition
  915. # -[0-9]+- ; <addition> --> confirmed manually with addition
  916. $cmdFavId = $1;
  917. $cmdAddition = $3;
  918. $isConfirm = 1;
  919. } elsif ( $cmd =~ /^\s*-([0-9]+)-( = ((=[^;])|([^=;])|([^=];))*)( =; (.*))?\s*$/ ) {
  920. # -[0-9]+- = <description or cmd> --> confirmed automatically without addition
  921. # -[0-9]+- = <description or cmd> =; <addition> --> confirmed automatically with addition
  922. $cmdFavId = $1;
  923. $cmdAddition = $8;
  924. $isConfirm = 1;
  925. } elsif ( $cmd =~ /^\s*(.*)\s*$/ ) {
  926. # --> no id no addition
  927. # <addition> --> no id but addition
  928. $cmdAddition = $1;
  929. }
  930. # trim cmd addition if given
  931. $cmdAddition =~ s/^\s+|\s+$//g if ( $cmdAddition );
  932. Log3 $name, 5, "TelegramBot_SendFavorites parsed cmdFavId :".(defined($cmdFavId)?$cmdFavId:"<undef>")." cmdaddition :".(defined($cmdAddition)?$cmdAddition:"<undef>").": ";
  933. # if given a number execute the numbered favorite as a command
  934. if ( $cmdFavId ) {
  935. my $cmdId = ($cmdFavId-1);
  936. Log3 $name, 4, "TelegramBot_SendFavorites exec cmd :$cmdId: ";
  937. if ( ( $cmdId >= 0 ) && ( $cmdId < scalar( @clist ) ) ) {
  938. my $ecmd = $clist[$cmdId];
  939. $ecmd =~ s/SeMiCoLoN/;;/g; # reestablish double ; for inside commands
  940. my ( $alias, $desc, $minusdesc, $parsecmd, $needsConfirm, $needsResult, $hidden ) = TelegramBot_SplitFavoriteDef( $hash, $ecmd );
  941. return "Alias could not be parsed :$ecmd:" if ( ! $parsecmd );
  942. $ecmd = $parsecmd;
  943. if ( ( $hidden ) && ( ! $aliasExec ) && ( ! $isConfirm ) && ( ! $storedMgsId ) ) {
  944. Log3 $name, 3, "TelegramBot_SendFavorites hidden favorite (id;".($cmdId+1).") execution from ".$mpeernorm;
  945. } elsif ( ( ! $isConfirm ) && ( $needsConfirm ) ) {
  946. # ask first for confirmation
  947. my $fcmd = AttrVal($name,'cmdFavorites',"");
  948. my @tmparr;
  949. my @keys = ();
  950. my $tmptxt;
  951. if ( $isInline ) {
  952. $tmptxt = (($desc)?$desc:$parsecmd);
  953. $tmptxt .= " (".($alias?$alias:$fcmd.$cmdFavId).")" if ( ! $minusdesc );
  954. $tmptxt .= ":TBOT_FAVORITE_-$cmdFavId";
  955. } else {
  956. $tmptxt = $fcmd."-".$cmdFavId."- = ".(($desc)?$desc:$parsecmd).($cmdAddition?" =; ".$cmdAddition:"");
  957. }
  958. my @tmparr1 = ( $tmptxt );
  959. push( @keys, \@tmparr1 );
  960. if ( $isInline ) {
  961. if ( $iscallback ) {
  962. $tmptxt = "Zurueck:TBOT_FAVORITE_MENU";
  963. } else {
  964. $tmptxt = "Abbruch:TBOT_FAVORITE_CANCEL";
  965. }
  966. } else {
  967. $tmptxt = "Abbruch";
  968. }
  969. my @tmparr2 = ( $tmptxt );
  970. push( @keys, \@tmparr2 );
  971. my $jsonkb = TelegramBot_MakeKeyboard( $hash, 1, $isInline, @keys );
  972. # LOCAL: External message
  973. $ret = encode_utf8( AttrVal( $name, 'textResponseConfirm', 'TelegramBot FHEM : $peer\n Soll der Befehl ausgeführt werden? \n') );
  974. $ret =~ s/\$peer/$resppeer/g;
  975. return TelegramBot_SendIt( $hash, (($mchatnorm)?$mchatnorm:$mpeernorm), $ret, $jsonkb,
  976. (($storedMgsId)?10:0), $storedMgsId, ((!$storedMgsId)?"-msgid-":"") );
  977. } else {
  978. # $ecmd = $1 if ( $ecmd =~ /^\s*\?(.*)$/ );
  979. if ( $storedMgsId ) {
  980. # 10 for edit inline
  981. $ret = TelegramBot_SendIt( $hash, (($mchatnorm)?$mchatnorm:$mpeernorm), "-", undef, 10, $storedMgsId );
  982. }
  983. $ecmd .= " ".$cmdAddition if ( $cmdAddition );
  984. return TelegramBot_ExecuteCommand( $hash, $mpeernorm, $mchatnorm, $ecmd, $needsResult, $storedMgsId );
  985. }
  986. } else {
  987. Log3 $name, 3, "TelegramBot_SendFavorites cmd id not defined :($cmdId+1): ";
  988. }
  989. }
  990. # ret not defined means no favorite found that matches cmd or no fav given in cmd
  991. if ( ! defined( $ret ) ) {
  992. my $cnt = 0;
  993. my @keys = ();
  994. my $showalsohidden = 1 if ( $cmdAddition && ( $cmdAddition =~ /^hidden$/i ) );
  995. my $fcmd = AttrVal($name,'cmdFavorites',undef);
  996. my $lastarr;
  997. foreach my $cs ( @clist ) {
  998. $cs =~ s/SeMiCoLoN/;;/g; # reestablish double ; for inside commands
  999. $cnt += 1;
  1000. my ( $alias, $desc, $minusdesc, $parsecmd, $needsConfirm, $needsResult, $hidden ) = TelegramBot_SplitFavoriteDef( $hash, $cs );
  1001. if ( ( defined($parsecmd) ) && ( ( ! $hidden ) || $showalsohidden ) ) {
  1002. my $key;
  1003. if ( $isInline ) {
  1004. $key = (($desc)?$desc:$parsecmd);
  1005. $key .= " (".($alias?$alias:$fcmd.$cnt).")" if ( ! $minusdesc );
  1006. $key .= ":TBOT_FAVORITE_$cnt";
  1007. } else {
  1008. $key = ( $fcmd.$cnt." = ".($alias?$alias." = ":"").(($desc)?$desc:$parsecmd) );
  1009. }
  1010. if ( ( $cnt % 2 ) || ( ! AttrVal($name,"favorites2Col",0) ) ){
  1011. my @tmparr = ( $key );
  1012. $lastarr = \@tmparr;
  1013. push( @keys, \@tmparr );
  1014. } else {
  1015. $$lastarr[1] = $key;
  1016. }
  1017. }
  1018. }
  1019. if ( $isInline ) {
  1020. my @tmparr = ( "Abbruch:TBOT_FAVORITE_CANCEL" );
  1021. push( @keys, \@tmparr );
  1022. }
  1023. my $jsonkb = TelegramBot_MakeKeyboard( $hash, 1, $isInline, @keys );
  1024. Log3 $name, 5, "TelegramBot_SendFavorites keyboard:".$jsonkb.": ";
  1025. # LOCAL: External message
  1026. $ret = AttrVal( $name, 'textResponseFavorites', 'TelegramBot FHEM : $peer\n Favoriten \n');
  1027. $ret =~ s/\$peer/$resppeer/g;
  1028. # always start new dialog if msgid is given
  1029. $ret = TelegramBot_SendIt( $hash, (($mchatnorm)?$mchatnorm:$mpeernorm), $ret, $jsonkb,
  1030. (($storedMgsId)?10:0), $storedMgsId, ((!$storedMgsId)?"-msgid-":"") );
  1031. }
  1032. return $ret;
  1033. }
  1034. #####################################
  1035. #####################################
  1036. # INTERNAL: handle sentlast and favorites
  1037. sub TelegramBot_SentLastCommand($$$$) {
  1038. my ($hash, $mpeernorm, $mchatnorm, $cmd ) = @_;
  1039. my $name = $hash->{NAME};
  1040. my $ret;
  1041. Log3 $name, 5, "TelegramBot_SentLastCommand cmd correct peer ";
  1042. my $slc = ReadingsVal($name ,"StoredCommands","");
  1043. my @cmds = split( "\n", $slc );
  1044. # create keyboard
  1045. my @keys = ();
  1046. foreach my $cs ( @cmds ) {
  1047. my @tmparr = ( $cs );
  1048. push( @keys, \@tmparr );
  1049. }
  1050. # my @tmparr = ( $fcmd."0 = Abbruch" );
  1051. # push( @keys, \@tmparr );
  1052. my $jsonkb = TelegramBot_MakeKeyboard( $hash, 1, 0, @keys );
  1053. # LOCAL: External message
  1054. $ret = AttrVal( $name, 'textResponseCommands', 'TelegramBot FHEM : $peer\n Letzte Befehle \n');
  1055. my $resppeer = $mpeernorm;
  1056. $resppeer .= "(".$mchatnorm.")" if ( $mchatnorm );
  1057. $ret =~ s/\$peer/$resppeer/g;
  1058. # overwrite ret with result from SendIt --> send response
  1059. $ret = TelegramBot_SendIt( $hash, (($mchatnorm)?$mchatnorm:$mpeernorm), $ret, $jsonkb, 0 );
  1060. return $ret;
  1061. }
  1062. #####################################
  1063. #####################################
  1064. # INTERNAL: execute command and sent return value
  1065. sub TelegramBot_ReadHandleCommand($$$$$) {
  1066. my ($hash, $mpeernorm, $mchatnorm, $cmd, $mtext ) = @_;
  1067. my $name = $hash->{NAME};
  1068. my $ret;
  1069. Log3 $name, 4, "TelegramBot_ReadHandleCommand $name: cmd found :".$cmd.": ";
  1070. Log3 $name, 5, "TelegramBot_ReadHandleCommand cmd correct peer ";
  1071. # Either no peer defined or cmdpeer matches peer for message -> good to execute
  1072. my $cto = AttrVal($name,'cmdTriggerOnly',"0");
  1073. if ( $cto eq '1' ) {
  1074. $cmd = "trigger ".$cmd;
  1075. }
  1076. Log3 $name, 5, "TelegramBot_ReadHandleCommand final cmd for analyze :".$cmd.": ";
  1077. # store last commands (original text)
  1078. TelegramBot_AddStoredCommands( $hash, $mtext );
  1079. $ret = TelegramBot_ExecuteCommand( $hash, $mpeernorm, $mchatnorm, $cmd );
  1080. return $ret;
  1081. }
  1082. #####################################
  1083. #####################################
  1084. # INTERNAL: execute command and sent return value
  1085. sub TelegramBot_ExecuteCommand($$$$;$$) {
  1086. my ($hash, $mpeernorm, $mchatnorm, $cmd, $sentemptyresult, $qMsgId ) = @_;
  1087. my $name = $hash->{NAME};
  1088. my $ret;
  1089. $sentemptyresult = AttrVal($name,'cmdReturnEmptyResult',1) if ( ! $sentemptyresult );
  1090. # get human readble name for peer
  1091. my $pname = TelegramBot_GetFullnameForContact( $hash, $mpeernorm );
  1092. Log3 $name, 5, "TelegramBot_ExecuteCommand final cmd for analyze :".$cmd.": ";
  1093. # special case shutdown caught here to avoid endless loop
  1094. $ret = "shutdown command can not be executed" if ( $cmd =~ /^shutdown/ );
  1095. # Execute command
  1096. my $isMediaStream = 0;
  1097. if ( ! defined( $ret ) ) {
  1098. # run replace set magic on command - first
  1099. my %dummy;
  1100. my ($err, @a) = ReplaceSetMagic(\%dummy, 0, ( $cmd ) );
  1101. if ( $err ) {
  1102. Log3 $name, 1, "TelegramBot_ExecuteCommand $name: parse cmd failed on ReplaceSetmagic with :$err: on :$cmd:";
  1103. } else {
  1104. $cmd = join(" ", @a);
  1105. Log3 $name, 4, "TelegramBot_ExecuteCommand $name: parse cmd returned :$cmd:";
  1106. }
  1107. $ret = AnalyzeCommandChain( $hash, $cmd );
  1108. # Check for image/doc/audio stream in return (-1 image
  1109. ( $isMediaStream ) = TelegramBot_IdentifyStream( $hash, $ret ) if ( defined( $ret ) );
  1110. Log3 $name, 3, "TelegramBot_ExecuteCommand $name: cmd executed :".$cmd.": --> :".TelegramBot_MsgForLog($ret, $isMediaStream ).":" if ( $ret );
  1111. }
  1112. Log3 $name, 4, "TelegramBot_ExecuteCommand result for analyze :".TelegramBot_MsgForLog($ret, $isMediaStream ).": ";
  1113. my $defpeer = AttrVal($name,'defaultPeer',undef);
  1114. $defpeer = TelegramBot_GetIdForPeer( $hash, $defpeer ) if ( defined( $defpeer ) );
  1115. $defpeer = AttrVal($name,'defaultPeer',undef) if ( ! defined( $defpeer ) );
  1116. $defpeer = undef if ( defined($defpeer) && ( $defpeer eq $mpeernorm ) );
  1117. # LOCAL: External message
  1118. my $retMsg = AttrVal( $name, 'textResponseResult', 'TelegramBot FHEM - $peer Befehl:$cmd: - Ergebnis:\n$result \n ');
  1119. $retMsg =~ s/\$cmd/$cmd/g;
  1120. if ( defined( $defpeer ) ) {
  1121. $retMsg =~ s/\$peer/$pname/g;
  1122. } else {
  1123. $retMsg =~ s/\$peer//g;
  1124. }
  1125. if ( ( ! defined( $ret ) ) || ( length( $ret) == 0 ) ) {
  1126. $retMsg =~ s/\$result/OK/g;
  1127. $ret = $retMsg if ( $sentemptyresult );
  1128. } elsif ( ! $isMediaStream ) {
  1129. $retMsg =~ s/\$result/$ret/g;
  1130. $ret = $retMsg;
  1131. }
  1132. # trim $ret avoiding empty msg error from telegram
  1133. # Debug "Length before :".length($ret)." :$ret:";
  1134. $ret =~ s/^(\s|(\\[rfnt]))+|(\s|(\\[rfnt]))+$//g if ( defined($ret) );
  1135. # Debug "Length after :".length($ret);
  1136. Log3 $name, 5, "TelegramBot_ExecuteCommand $name: ".TelegramBot_MsgForLog($ret, $isMediaStream ).": ";
  1137. if ( ( defined( $ret ) ) && ( length( $ret) != 0 ) ) {
  1138. if ( ! $isMediaStream ) {
  1139. # replace line ends with spaces
  1140. $ret =~ s/\r//gm;
  1141. # shorten to maxReturnSize if set
  1142. my $limit = AttrVal($name,'maxReturnSize',4000);
  1143. if ( ( length($ret) > $limit ) && ( $limit != 0 ) ) {
  1144. $ret = substr( $ret, 0, $limit )."\n \n ...";
  1145. }
  1146. $ret =~ s/\n/\\n/gm;
  1147. }
  1148. my $peers = (($mchatnorm)?$mchatnorm:$mpeernorm);
  1149. my $dpc = AttrVal($name,'defaultPeerCopy',1);
  1150. $peers .= " ".$defpeer if ( ( $dpc ) && ( defined( $defpeer ) ) );
  1151. if ( $isMediaStream ) {
  1152. # changing message only if not mediastream
  1153. $qMsgId = undef;
  1154. } elsif ( $qMsgId ) {
  1155. # set to 10 if query msg id is set
  1156. $isMediaStream = 10 # queryEditInline
  1157. }
  1158. # Ignore result from sendIt here
  1159. my $retsend = TelegramBot_SendIt( $hash, $peers, $ret, undef, $isMediaStream, $qMsgId );
  1160. # ensure return is not a stream (due to log handling)
  1161. $ret = TelegramBot_MsgForLog($ret, $isMediaStream )
  1162. }
  1163. return $ret;
  1164. }
  1165. ######################################
  1166. # add a command to the StoredCommands reading
  1167. # hash, cmd
  1168. sub TelegramBot_AddStoredCommands($$) {
  1169. my ($hash, $cmd) = @_;
  1170. my $stcmds = ReadingsVal($hash->{NAME},"StoredCommands","");
  1171. $stcmds = $stcmds;
  1172. if ( $stcmds !~ /^\Q$cmd\E$/m ) {
  1173. # add new cmd
  1174. $stcmds .= $cmd."\n";
  1175. # check number lines
  1176. my $num = ( $stcmds =~ tr/\n// );
  1177. if ( $num > 10 ) {
  1178. $stcmds =~ /^[^\n]+\n(.*)$/s;
  1179. $stcmds = $1;
  1180. }
  1181. # change reading
  1182. readingsSingleUpdate($hash, "StoredCommands", $stcmds , 1);
  1183. Log3 $hash->{NAME}, 4, "TelegramBot_AddStoredCommands :$stcmds: ";
  1184. }
  1185. }
  1186. #####################################
  1187. # INTERNAL: Function to check for commands in messages
  1188. # Always executes and returns on first match also in case of error
  1189. # mid contains the message that might be needed to editinline for favorites
  1190. sub Telegram_HandleCommandInMessages($$$$$$)
  1191. {
  1192. my ( $hash, $mpeernorm, $mchatnorm, $mtext, $mid, $iscallback ) = @_;
  1193. my $name = $hash->{NAME};
  1194. my $cmdRet;
  1195. my $cmd;
  1196. my $doRet;
  1197. # trim whitespace from message text
  1198. $mtext =~ s/^\s+|\s+$//g;
  1199. #### Check authorization for cmd execution is done inside checkCmdKeyword
  1200. # Check for cmdKeyword in msg
  1201. ( $cmd, $doRet ) = TelegramBot_checkCmdKeyword( $hash, $mpeernorm, $mchatnorm, $mtext, AttrVal($name,'cmdKeyword',undef), 1 );
  1202. if ( defined( $cmd ) ) {
  1203. $cmdRet = TelegramBot_ReadHandleCommand( $hash, $mpeernorm, $mchatnorm, $cmd, $mtext );
  1204. Log3 $name, 4, "TelegramBot_ParseMsg $name: ReadHandleCommand returned :$cmdRet:" if ( defined($cmdRet) );
  1205. return;
  1206. } elsif ( $doRet ) {
  1207. return;
  1208. }
  1209. # Check for sentCommands Keyword in msg
  1210. ( $cmd, $doRet ) = TelegramBot_checkCmdKeyword( $hash, $mpeernorm, $mchatnorm, $mtext, AttrVal($name,'cmdSentCommands',undef), 1 );
  1211. if ( defined( $cmd ) ) {
  1212. $cmdRet = TelegramBot_SentLastCommand( $hash, $mpeernorm, $mchatnorm, $cmd );
  1213. Log3 $name, 4, "TelegramBot_ParseMsg $name: SentLastCommand returned :$cmdRet:" if ( defined($cmdRet) );
  1214. return;
  1215. } elsif ( $doRet ) {
  1216. return;
  1217. }
  1218. # Check for favorites Keyword in msg
  1219. ( $cmd, $doRet ) = TelegramBot_checkCmdKeyword( $hash, $mpeernorm, $mchatnorm, $mtext, AttrVal($name,'cmdFavorites',undef), 0 );
  1220. if ( defined( $cmd ) ) {
  1221. $cmdRet = TelegramBot_SendFavorites( $hash, $mpeernorm, $mchatnorm, $cmd, $mid, 0, $iscallback );
  1222. Log3 $name, 4, "TelegramBot_ParseMsg $name: SendFavorites returned :$cmdRet:" if ( defined($cmdRet) );
  1223. return;
  1224. } elsif ( $doRet ) {
  1225. return;
  1226. }
  1227. # Check for favorite aliase in msg - execute command then
  1228. if ( defined( $hash->{AliasCmds} ) ) {
  1229. foreach my $aliasKey (keys %{$hash->{AliasCmds}} ) {
  1230. ( $cmd, $doRet ) = TelegramBot_checkCmdKeyword( $hash, $mpeernorm, $mchatnorm, $mtext, $aliasKey, 1 );
  1231. if ( defined( $cmd ) ) {
  1232. $cmd = $hash->{AliasCmds}{$aliasKey}." ".$cmd;
  1233. $cmdRet = TelegramBot_SendFavorites( $hash, $mpeernorm, $mchatnorm, $cmd, $mid, 1, $iscallback ); # call with aliasesxec set
  1234. Log3 $name, 4, "TelegramBot_ParseMsg $name: SendFavorites (alias) returned :$cmdRet:" if ( defined($cmdRet) );
  1235. return;
  1236. } elsif ( $doRet ) {
  1237. return;
  1238. }
  1239. }
  1240. }
  1241. # ignore result of readhandlecommand since it leads to endless loop
  1242. }
  1243. #####################################
  1244. # INTERNAL: Function to send a command handle result
  1245. # Parameter
  1246. # hash
  1247. # url - url including parameters
  1248. # > returns string in case of error or the content of the result object if ok
  1249. # ignore set means no error is logged
  1250. sub TelegramBot_DoUrlCommand($$;$)
  1251. {
  1252. my ( $hash, $url, $ignore ) = @_;
  1253. my $name = $hash->{NAME};
  1254. my $ret;
  1255. Log3 $name, 5, "TelegramBot_DoUrlCommand $name: called ";
  1256. my $param = {
  1257. url => $url,
  1258. timeout => 1,
  1259. hash => $hash,
  1260. method => "GET",
  1261. header => $TelegramBot_header
  1262. };
  1263. my ($err, $data) = HttpUtils_BlockingGet( $param );
  1264. if ( $err ne "" ) {
  1265. # http returned error
  1266. $ret = "FAILED http access returned error :$err:";
  1267. Log3 $name, ($ignore?5:2), "TelegramBot_DoUrlCommand $name: ".$ret;
  1268. } else {
  1269. my $jo;
  1270. eval {
  1271. # $jo = decode_json( $data );
  1272. my $json = JSON->new->allow_nonref;
  1273. $jo = $json->decode(Encode::encode_utf8($data));
  1274. };
  1275. if ( ! defined( $jo ) ) {
  1276. $ret = "FAILED invalid JSON returned";
  1277. Log3 $name, ($ignore?5:2), "TelegramBot_DoUrlCommand $name: ".$ret;
  1278. } elsif ( $jo->{ok} ) {
  1279. $ret = $jo->{result};
  1280. Log3 $name, 4, "TelegramBot_DoUrlCommand OK with result";
  1281. } else {
  1282. my $ret = "FAILED Telegram returned error: ".$jo->{description};
  1283. Log3 $name, ($ignore?5:2), "TelegramBot_DoUrlCommand $name: ".$ret;
  1284. }
  1285. }
  1286. return $ret;
  1287. }
  1288. ##############################################################################
  1289. ##############################################################################
  1290. ##
  1291. ## Communication - Send - receive - Parse
  1292. ##
  1293. ##############################################################################
  1294. ##############################################################################
  1295. #####################################
  1296. # INTERNAL: Function to send a photo (and text message) to a peer and handle result
  1297. # addPar is caption for images / keyboard for text / longituted for location (isMedia 10)
  1298. # isMedia - 0 (text)
  1299. # $options is list of options in hyphens --> e.g. -msgid- for get msgid
  1300. sub TelegramBot_SendIt($$$$$;$$$)
  1301. {
  1302. my ( $hash, @args) = @_;
  1303. my ( $peers, $msg, $addPar, $isMedia, $replyid, $options, $retryCount) = @args;
  1304. my $name = $hash->{NAME};
  1305. $retryCount = 0 if ( ! defined($retryCount) );
  1306. $options = "" if ( ! defined($options) );
  1307. # increase retrycount for next try
  1308. $args[$TelegramBot_arg_retrycnt] = $retryCount+1;
  1309. Log3 $name, 5, "TelegramBot_SendIt $name: called ";
  1310. # ignore all sends if disabled
  1311. return if ( AttrVal($name,'disable',0) );
  1312. # ensure sentQueue exists
  1313. $hash->{sentQueue} = [] if ( ! defined( $hash->{sentQueue} ) );
  1314. if ( ( defined( $hash->{sentMsgResult} ) ) && ( $hash->{sentMsgResult} =~ /^WAITING/ ) && ( $retryCount == 0 ) ){
  1315. # add to queue
  1316. Log3 $name, 4, "TelegramBot_SendIt $name: add send to queue :$peers: -:".
  1317. TelegramBot_MsgForLog($msg, ($isMedia<0) ).": - :".(defined($addPar)?$addPar:"<undef>").":";
  1318. push( @{ $hash->{sentQueue} }, \@args );
  1319. return;
  1320. }
  1321. my $ret;
  1322. $hash->{sentMsgResult} = "WAITING";
  1323. $hash->{sentMsgResult} .= " retry $retryCount" if ( $retryCount > 0 );
  1324. $hash->{sentMsgId} = "";
  1325. my $peer;
  1326. ( $peer, $peers ) = split( " ", $peers, 2 );
  1327. # handle addtl peers specified (will be queued since WAITING is set already)
  1328. if ( defined( $peers ) ) {
  1329. # ignore return, since it is only queued
  1330. # remove msgid from options and also replyid reset
  1331. my $sepoptions = $options;
  1332. $sepoptions =~ s/-msgid-//;
  1333. TelegramBot_SendIt( $hash, $peers, $msg, $addPar, $isMedia, undef, $sepoptions );
  1334. }
  1335. Log3 $name, 5, "TelegramBot_SendIt $name: try to send message to :$peer: -:".
  1336. TelegramBot_MsgForLog($msg, ($isMedia<0) ).": - add :".(defined($addPar)?$addPar:"<undef>").
  1337. ": - replyid :".(defined($replyid)?$replyid:"<undef>").
  1338. ":".": options :".$options.":";
  1339. # trim and convert spaces in peer to underline
  1340. $peer = 0 if ( ! $peer ); # ensure peer is defined
  1341. my $peer2 = (! $peer )?$peer:TelegramBot_GetIdForPeer( $hash, $peer );
  1342. # Debug "peer :$peer: peer2 :$peer2:";
  1343. if ( ! defined( $peer2 ) ) {
  1344. $ret = "FAILED peer not found :$peer:";
  1345. # Log3 $name, 2, "TelegramBot_SendIt $name: failed with :".$ret.":";
  1346. $peer2 = "";
  1347. }
  1348. $hash->{sentMsgPeer} = TelegramBot_GetFullnameForContact( $hash, $peer2 );
  1349. $hash->{sentMsgPeerId} = $peer2;
  1350. $hash->{sentMsgOptions} = $options;
  1351. # init param hash
  1352. $hash->{HU_DO_PARAMS}->{hash} = $hash;
  1353. $hash->{HU_DO_PARAMS}->{header} = $TelegramBot_header;
  1354. delete( $hash->{HU_DO_PARAMS}->{args} );
  1355. delete( $hash->{HU_DO_PARAMS}->{boundary} );
  1356. my $timeout = AttrVal($name,'cmdTimeout',30);
  1357. $hash->{HU_DO_PARAMS}->{timeout} = $timeout;
  1358. $hash->{HU_DO_PARAMS}->{loglevel} = 4;
  1359. # Debug option - switch this on for detailed logging of httputils
  1360. # $hash->{HU_DO_PARAMS}->{loglevel} = 1;
  1361. # handle data creation only if no error so far
  1362. if ( ! defined( $ret ) ) {
  1363. # add chat / user id (no file) --> this will also do init
  1364. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "chat_id", undef, $peer2, 0 ) if ( $peer );
  1365. if ( ( $isMedia == 0 ) || ( $isMedia == 10 ) || ( $isMedia == 20 ) ) {
  1366. if ( $isMedia == 0 ) {
  1367. $hash->{HU_DO_PARAMS}->{url} = TelegramBot_getBaseURL($hash)."sendMessage";
  1368. } elsif ( $isMedia == 20 ) {
  1369. $hash->{HU_DO_PARAMS}->{url} = TelegramBot_getBaseURL($hash)."deleteMessage";
  1370. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "message_id", undef, $replyid, 0 ) if ( ! defined( $ret ) );
  1371. $replyid = undef;
  1372. } else {
  1373. $hash->{HU_DO_PARAMS}->{url} = TelegramBot_getBaseURL($hash)."editMessageText";
  1374. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "message_id", undef, $replyid, 0 ) if ( ! defined( $ret ) );
  1375. $replyid = undef;
  1376. }
  1377. # DEBUG OPTION
  1378. # $hash->{HU_DO_PARAMS}->{url} = "http://requestb.in/1ibjnj81" if ( $msg =~ /^ZZZ/ );
  1379. my $parseMode = TelegramBot_AttrNum($name,"parseModeSend","0" );
  1380. if ( $parseMode == 1 ) {
  1381. $parseMode = "Markdown";
  1382. } elsif ( $parseMode == 2 ) {
  1383. $parseMode = "HTML";
  1384. } elsif ( $parseMode == 3 ) {
  1385. $parseMode = 0;
  1386. if ( $msg =~ /^markdown(.*)$/i ) {
  1387. $msg = $1;
  1388. $parseMode = "Markdown";
  1389. } elsif ( $msg =~ /^HTML(.*)$/i ) {
  1390. $msg = $1;
  1391. $parseMode = "HTML";
  1392. }
  1393. } else {
  1394. $parseMode = 0;
  1395. }
  1396. Log3 $name, 4, "TelegramBot_SendIt parseMode $parseMode";
  1397. if ( length($msg) > 1000 ) {
  1398. $hash->{sentMsgText} = substr($msg,0, 1000)."...";
  1399. } else {
  1400. $hash->{sentMsgText} = $msg;
  1401. }
  1402. $msg =~ s/(?<![\\])\\n/\x0A/g;
  1403. $msg =~ s/(?<![\\])\\t/\x09/g;
  1404. # add msg (no file)
  1405. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "text", undef, $msg, 0 ) if ( ! defined( $ret ) );
  1406. # add parseMode
  1407. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "parse_mode", undef, $parseMode, 0 ) if ( ( ! defined( $ret ) ) && ( $parseMode ) );
  1408. # add disable_web_page_preview
  1409. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "disable_web_page_preview", undef, JSON::true, 0 )
  1410. if ( ( ! defined( $ret ) ) && ( ! AttrVal($name,'webPagePreview',1) ) );
  1411. } elsif ( $isMedia == 11 ) {
  1412. # Location send
  1413. $hash->{sentMsgText} = "Location: ".TelegramBot_MsgForLog($msg, ($isMedia<0) ).
  1414. (( defined( $addPar ) )?" - ".$addPar:"");
  1415. $hash->{HU_DO_PARAMS}->{url} = TelegramBot_getBaseURL($hash)."sendLocation";
  1416. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "latitude", undef, $msg, 0 ) if ( ! defined( $ret ) );
  1417. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "longitude", undef, $addPar, 0 ) if ( ! defined( $ret ) );
  1418. $addPar = undef;
  1419. } elsif ( $isMedia == 12 ) {
  1420. # answer Inline query
  1421. $hash->{sentMsgText} = "AnswerInline: ".TelegramBot_MsgForLog($msg, ($isMedia<0) ).
  1422. (( defined( $addPar ) )?" - ".$addPar:"");
  1423. $hash->{HU_DO_PARAMS}->{url} = TelegramBot_getBaseURL($hash)."answerCallbackQuery";
  1424. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "callback_query_id", undef, $addPar, 0 ) if ( ! defined( $ret ) );
  1425. $addPar = undef;
  1426. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "text", undef, $msg, 0 ) if ( ( ! defined( $ret ) ) && ( $msg ) );
  1427. } elsif ( abs($isMedia) == 1 ) {
  1428. # Photo send
  1429. $hash->{sentMsgText} = "Image: ".TelegramBot_MsgForLog($msg, ($isMedia<0) ).
  1430. (( defined( $addPar ) )?" - ".$addPar:"");
  1431. $hash->{HU_DO_PARAMS}->{url} = TelegramBot_getBaseURL($hash)."sendPhoto";
  1432. # add caption
  1433. if ( defined( $addPar ) ) {
  1434. $addPar =~ s/(?<![\\])\\n/\x0A/g;
  1435. $addPar =~ s/(?<![\\])\\t/\x09/g;
  1436. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "caption", undef, $addPar, 0 ) if ( ! defined( $ret ) );
  1437. $addPar = undef;
  1438. }
  1439. # add msg or file or stream
  1440. Log3 $name, 4, "TelegramBot_SendIt $name: Filename for image file :".
  1441. TelegramBot_MsgForLog($msg, ($isMedia<0) ).":";
  1442. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "photo", undef, $msg, $isMedia ) if ( ! defined( $ret ) );
  1443. } elsif ( $isMedia == 2 ) {
  1444. # Voicemsg send == 2
  1445. $hash->{sentMsgText} = "Voice: $msg";
  1446. $hash->{HU_DO_PARAMS}->{url} = TelegramBot_getBaseURL($hash)."sendVoice";
  1447. # add msg or file or stream
  1448. Log3 $name, 4, "TelegramBot_SendIt $name: Filename for document file :".
  1449. TelegramBot_MsgForLog($msg, ($isMedia<0) ).":";
  1450. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "voice", undef, $msg, 1 ) if ( ! defined( $ret ) );
  1451. } else {
  1452. # Media send == 3
  1453. $hash->{sentMsgText} = "Document: ".TelegramBot_MsgForLog($msg, ($isMedia<0) );
  1454. $hash->{HU_DO_PARAMS}->{url} = TelegramBot_getBaseURL($hash)."sendDocument";
  1455. # add msg (no file)
  1456. Log3 $name, 4, "TelegramBot_SendIt $name: Filename for document file :$msg:";
  1457. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "document", undef, $msg, $isMedia ) if ( ! defined( $ret ) );
  1458. }
  1459. if ( defined( $replyid ) ) {
  1460. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "reply_to_message_id", undef, $replyid, 0 ) if ( ! defined( $ret ) );
  1461. }
  1462. if ( defined( $addPar ) ) {
  1463. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "reply_markup", undef, $addPar, 0 ) if ( ! defined( $ret ) );
  1464. } elsif ( $options =~ /-force_reply-/ ) {
  1465. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "reply_markup", undef, "{\"force_reply\":true}", 0 ) if ( ! defined( $ret ) );
  1466. }
  1467. if ( $options =~ /-silent-/ ) {
  1468. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, "disable_notification", undef, "true", 0 ) if ( ! defined( $ret ) );
  1469. }
  1470. # finalize multipart
  1471. $ret = TelegramBot_AddMultipart($hash, $hash->{HU_DO_PARAMS}, undef, undef, undef, 0 ) if ( ! defined( $ret ) );
  1472. }
  1473. if ( defined( $ret ) ) {
  1474. Log3 $name, 3, "TelegramBot_SendIt $name: Failed with :$ret:";
  1475. TelegramBot_Callback( $hash->{HU_DO_PARAMS}, $ret, "");
  1476. } else {
  1477. $hash->{HU_DO_PARAMS}->{args} = \@args;
  1478. # if utf8 is set on string this will lead to length wrongly calculated in HTTPUtils (char instead of bytes) for some installations
  1479. if ( ( AttrVal($name,'utf8Special',0) ) && ( utf8::is_utf8($hash->{HU_DO_PARAMS}->{data}) ) ) {
  1480. Log3 $name, 4, "TelegramBot_SendIt $name: utf8 encoding for data in message ";
  1481. utf8::downgrade($hash->{HU_DO_PARAMS}->{data});
  1482. # $hash->{HU_DO_PARAMS}->{data} = encode_utf8($hash->{HU_DO_PARAMS}->{data});
  1483. }
  1484. Log3 $name, 4, "TelegramBot_SendIt $name: timeout for sent :".$hash->{HU_DO_PARAMS}->{timeout}.": ";
  1485. HttpUtils_NonblockingGet( $hash->{HU_DO_PARAMS} );
  1486. }
  1487. return $ret;
  1488. }
  1489. #####################################
  1490. # INTERNAL: Build a multipart form data in a given hash
  1491. # Parameter
  1492. # hash (device hash)
  1493. # params (hash for building up the data)
  1494. # paramname --> if not sepecifed / undef - multipart will be finished
  1495. # header for multipart
  1496. # content
  1497. # isFile to specify if content is providing a file to be read as content
  1498. #
  1499. # > returns string in case of error or undef
  1500. sub TelegramBot_AddMultipart($$$$$$)
  1501. {
  1502. my ( $hash, $params, $parname, $parheader, $parcontent, $isMedia ) = @_;
  1503. my $name = $hash->{NAME};
  1504. my $ret;
  1505. # Check if boundary is defined
  1506. if ( ! defined( $params->{boundary} ) ) {
  1507. $params->{boundary} = "TelegramBot_boundary-x0123";
  1508. $params->{header} .= "\r\nContent-Type: multipart/form-data; boundary=".$params->{boundary};
  1509. $params->{method} = "POST";
  1510. $params->{data} = "";
  1511. }
  1512. # ensure parheader is defined and add final header new lines
  1513. $parheader = "" if ( ! defined( $parheader ) );
  1514. $parheader .= "\r\n" if ( ( length($parheader) > 0 ) && ( $parheader !~ /\r\n$/ ) );
  1515. # add content
  1516. my $finalcontent;
  1517. if ( defined( $parname ) ) {
  1518. $params->{data} .= "--".$params->{boundary}."\r\n";
  1519. if ( $isMedia > 0) {
  1520. # url decode filename
  1521. $parcontent = uri_unescape($parcontent) if ( AttrVal($name,'filenameUrlEscape',0) );
  1522. my $baseFilename = basename($parcontent);
  1523. $parheader = "Content-Disposition: form-data; name=\"".$parname."\"; filename=\"".$baseFilename."\"\r\n".$parheader."\r\n";
  1524. return( "FAILED file :$parcontent: not found or empty" ) if ( ! -e $parcontent ) ;
  1525. my $size = -s $parcontent;
  1526. my $limit = AttrVal($name,'maxFileSize',10485760);
  1527. return( "FAILED file :$parcontent: is too large for transfer (current limit: ".$limit."B)" ) if ( $size > $limit ) ;
  1528. $finalcontent = TelegramBot_BinaryFileRead( $hash, $parcontent );
  1529. if ( $finalcontent eq "" ) {
  1530. return( "FAILED file :$parcontent: not found or empty" );
  1531. }
  1532. } elsif ( $isMedia < 0) {
  1533. my ( $im, $ext ) = TelegramBot_IdentifyStream( $hash, $parcontent );
  1534. my $baseFilename = "fhem.".$ext;
  1535. $parheader = "Content-Disposition: form-data; name=\"".$parname."\"; filename=\"".$baseFilename."\"\r\n".$parheader."\r\n";
  1536. $finalcontent = $parcontent;
  1537. } else {
  1538. $parheader = "Content-Disposition: form-data; name=\"".$parname."\"\r\n".$parheader."\r\n";
  1539. $finalcontent = $parcontent;
  1540. }
  1541. $params->{data} .= $parheader.$finalcontent."\r\n";
  1542. } else {
  1543. return( "No content defined for multipart" ) if ( length( $params->{data} ) == 0 );
  1544. $params->{data} .= "--".$params->{boundary}."--";
  1545. }
  1546. return undef;
  1547. }
  1548. #####################################
  1549. # INTERNAL: Build a keyboard string for sendMessage
  1550. # Parameter
  1551. # hash (device hash)
  1552. # onetime/hide --> true means onetime / false means hide / undef means nothing
  1553. # inline --> true/false
  1554. # keys array of arrays for keyboard
  1555. # > returns string in case of error or undef
  1556. sub TelegramBot_MakeKeyboard($$$@)
  1557. {
  1558. my ( $hash, $onetime_hide, $inlinekb, @keys ) = @_;
  1559. my $name = $hash->{NAME};
  1560. my $ret;
  1561. my %par;
  1562. if ( ( defined( $inlinekb ) ) && ( $inlinekb ) ) {
  1563. # inline kb
  1564. my @parKeys = ( );
  1565. my $keytext;
  1566. my $keydata;
  1567. foreach my $aKeyRow ( @keys ) {
  1568. my @parRow = ();
  1569. foreach my $aKey ( @$aKeyRow ) {
  1570. $keytext = $aKey;
  1571. if ( $keytext =~ /^\s*(.*):([^:]+)\s*$/ ) {
  1572. $keytext = $1;
  1573. $keydata = $2;
  1574. } else {
  1575. $keydata = $keytext;
  1576. }
  1577. my %oneKey = ( "text" => $keytext, "callback_data" => $keydata );
  1578. push( @parRow, \%oneKey );
  1579. }
  1580. push( @parKeys, \@parRow );
  1581. }
  1582. %par = ( "inline_keyboard" => \@parKeys );
  1583. } elsif ( ( defined( $onetime_hide ) ) && ( ! $onetime_hide ) ) {
  1584. %par = ( "hide_keyboard" => JSON::true );
  1585. } else {
  1586. return $ret if ( ! @keys );
  1587. %par = ( "one_time_keyboard" => (( ( defined( $onetime_hide ) ) && ( $onetime_hide ) )?JSON::true:JSON::true ) );
  1588. $par{keyboard} = \@keys;
  1589. }
  1590. my $refkb = \%par;
  1591. # encode keyboard with JSON
  1592. my $json = JSON->new->utf8->allow_nonref;
  1593. eval {
  1594. $ret = $json->utf8(0)->encode( $refkb );
  1595. };
  1596. Log3 $name, 2, "JSON encode() did fail with: ".(( $@ )?$@:"<unknown error>") if ( ! $ret );
  1597. if ( $ret ) {
  1598. Log3 $name, 4, "TelegramBot_MakeKeyboard $name: json :$ret: is utf8? ".(utf8::is_utf8($ret)?"yes":"no");
  1599. if ( utf8::is_utf8($ret) ) {
  1600. utf8::downgrade($ret);
  1601. Log3 $name, 4, "TelegramBot_MakeKeyboard $name: json downgraded :$ret: is utf8? ".(utf8::is_utf8($ret)?"yes":"no");
  1602. }
  1603. }
  1604. return $ret;
  1605. }
  1606. #####################################
  1607. # INTERNAL: _PollUpdate is called to set out a nonblocking http call for updates
  1608. # if still polling return
  1609. # if more than one fails happened --> wait instead of poll
  1610. #
  1611. # 2nd parameter set means do it once only not by regular update
  1612. sub TelegramBot_UpdatePoll($;$)
  1613. {
  1614. my ($hash, $doOnce) = @_;
  1615. my $name = $hash->{NAME};
  1616. Log3 $name, 5, "TelegramBot_UpdatePoll $name: called ";
  1617. if ( $hash->{POLLING} ) {
  1618. Log3 $name, 4, "TelegramBot_UpdatePoll $name: polling still running ";
  1619. return ( ( $doOnce ) ? "Update polling still running" : undef );
  1620. }
  1621. # Get timeout from attribute
  1622. my $timeout = AttrVal($name,'pollingTimeout',0);
  1623. $timeout = 0 if ( AttrVal($name,'disable',0) );
  1624. if ( $doOnce ) {
  1625. $timeout = 0;
  1626. } else {
  1627. if ( $timeout == 0 ) {
  1628. $hash->{STATE} = "Static";
  1629. Log3 $name, 4, "TelegramBot_UpdatePoll $name: Polling timeout 0 - no polling ";
  1630. return;
  1631. }
  1632. if ( $hash->{FAILS} > 1 ) {
  1633. # more than one fail in a row wait until next poll
  1634. $hash->{OLDFAILS} = $hash->{FAILS};
  1635. $hash->{FAILS} = 0;
  1636. my $wait = $hash->{OLDFAILS}+2;
  1637. Log3 $name, 5, "TelegramBot_UpdatePoll $name: got fails :".$hash->{OLDFAILS}.": wait ".$wait." seconds";
  1638. InternalTimer(gettimeofday()+$wait, "TelegramBot_UpdatePoll", $hash,0);
  1639. return;
  1640. } elsif ( defined($hash->{OLDFAILS}) ) {
  1641. # oldfails defined means
  1642. $hash->{FAILS} = $hash->{OLDFAILS};
  1643. delete $hash->{OLDFAILS};
  1644. }
  1645. }
  1646. Log3 $name, 5, "TelegramBot_UpdatePoll $name: - Initiate non blocking polling - ".(defined($hash->{HU_UPD_PARAMS}->{callback})?"With callback set":"no callback");
  1647. # get next offset id
  1648. my $offset = $hash->{offset_id};
  1649. $offset = 0 if ( ! defined($offset) );
  1650. # build url
  1651. my $url = TelegramBot_getBaseURL($hash)."getUpdates?offset=".$offset. ( ($timeout!=0)? "&limit=5&timeout=".$timeout : "" );
  1652. $hash->{HU_UPD_PARAMS}->{url} = $url;
  1653. $hash->{HU_UPD_PARAMS}->{timeout} = $timeout+$timeout+5;
  1654. $hash->{HU_UPD_PARAMS}->{hash} = $hash;
  1655. $hash->{HU_UPD_PARAMS}->{offset} = $offset;
  1656. $hash->{STATE} = "Polling";
  1657. $hash->{HU_UPD_PARAMS}->{loglevel} = 4;
  1658. # Debug option - switch this on for detailed logging of httputils
  1659. # $hash->{HU_UPD_PARAMS}->{loglevel} = 1;
  1660. $hash->{POLLING} = ( ( defined( $hash->{OLD_POLLING} ) )?$hash->{OLD_POLLING}:1 );
  1661. Log3 $name, 4, "TelegramBot_UpdatePoll $name: initiate polling with nonblockingGet with ".$timeout."s";
  1662. HttpUtils_NonblockingGet( $hash->{HU_UPD_PARAMS} );
  1663. Log3 $name, 5, "TelegramBot_UpdatePoll $name: - Ende > next polling started";
  1664. }
  1665. #####################################
  1666. # INTERNAL: Called to retry a send operation after wait time
  1667. # Gets the do params
  1668. sub TelegramBot_RetrySend($)
  1669. {
  1670. my ( $param ) = @_;
  1671. my $hash= $param->{hash};
  1672. my $name = $hash->{NAME};
  1673. my $ref = $param->{args};
  1674. Log3 $name, 4, "TelegramBot_Retrysend $name: reply ".(defined( @$ref[4] )?@$ref[4]:"<undef>")." retry ".@$ref[$TelegramBot_arg_retrycnt]." :@$ref[0]: -:@$ref[1]: ";
  1675. TelegramBot_SendIt( $hash, @$ref[0], @$ref[1], @$ref[2], @$ref[3], @$ref[4], @$ref[5], @$ref[6] );
  1676. }
  1677. sub TelegramBot_Deepencode
  1678. {
  1679. my @result;
  1680. my $name = shift( @_ );
  1681. # Debug "TelegramBot_Deepencode with :".(@_).":";
  1682. for (@_) {
  1683. my $reftype= ref $_;
  1684. if( $reftype eq "ARRAY" ) {
  1685. Log3 $name, 5, "TelegramBot_Deepencode $name: found an ARRAY";
  1686. push @result, [ TelegramBot_Deepencode($name, @$_) ];
  1687. }
  1688. elsif( $reftype eq "HASH" ) {
  1689. my %h;
  1690. @h{keys %$_}= TelegramBot_Deepencode($name, values %$_);
  1691. Log3 $name, 5, "TelegramBot_Deepencode $name: found a HASH";
  1692. push @result, \%h;
  1693. }
  1694. else {
  1695. my $us = $_ ;
  1696. if ( utf8::is_utf8($us) ) {
  1697. $us = encode_utf8( $_ );
  1698. }
  1699. Log3 $name, 5, "TelegramBot_Deepencode $name: encoded a String from :".$_.": to :".$us.":";
  1700. push @result, $us;
  1701. }
  1702. }
  1703. return @_ == 1 ? $result[0] : @result;
  1704. }
  1705. #####################################
  1706. # INTERNAL: Callback is the callback for any nonblocking call to the bot api (e.g. the long poll on update call)
  1707. # 3 params are defined for callbacks
  1708. # param-hash
  1709. # err
  1710. # data (returned from url call)
  1711. # empty string used instead of undef for no return/err value
  1712. sub TelegramBot_Callback($$$)
  1713. {
  1714. my ( $param, $err, $data ) = @_;
  1715. my $hash= $param->{hash};
  1716. my $name = $hash->{NAME};
  1717. my $ret;
  1718. my $doRetry = 1; # will be set to zero if error is found that should lead to no retry
  1719. my $result;
  1720. my $msgId;
  1721. my $ll = 5;
  1722. if ( defined( $param->{isPolling} ) ) {
  1723. $hash->{OLD_POLLING} = ( ( defined( $hash->{POLLING} ) )?$hash->{POLLING}:0 ) + 1;
  1724. $hash->{OLD_POLLING} = 1 if ( $hash->{OLD_POLLING} > 255 );
  1725. $hash->{POLLING} = 0 if ( $hash->{POLLING} != -1 ) ;
  1726. }
  1727. Log3 $name, 5, "TelegramBot_Callback $name: called from ".(( defined( $param->{isPolling} ) )?"Polling":"SendIt");
  1728. # Check for timeout "read from $hash->{addr} timed out"
  1729. if ( $err =~ /^read from.*timed out$/ ) {
  1730. $ret = "NonBlockingGet timed out on read from ".($param->{hideurl}?"<hidden>":$param->{url})." after ".$param->{timeout}."s";
  1731. } elsif ( $err ne "" ) {
  1732. $ret = "NonBlockingGet: returned $err";
  1733. } elsif ( $data ne "" ) {
  1734. # assuming empty data without err means timeout
  1735. Log3 $name, 5, "TelegramBot_Callback $name: data returned :$data:";
  1736. my $jo;
  1737. eval {
  1738. # $data = encode( 'latin1', $data );
  1739. $data = encode_utf8( $data );
  1740. # $data = decode_utf8( $data );
  1741. # Debug "-----AFTER------\n".$data."\n-------UC=".${^UNICODE} ."-----\n";
  1742. # $jo = decode_json( $data );
  1743. my $json = JSON->new->allow_nonref;
  1744. $jo = $json->decode(Encode::encode_utf8($data));
  1745. $jo = TelegramBot_Deepencode( $name, $jo );
  1746. };
  1747. Log3 $name, 5, "TelegramBot_Callback $name: after encoding";
  1748. ######################
  1749. if ( $@ ) {
  1750. $ret = "Callback returned no valid JSON: $@ ";
  1751. } elsif ( ! defined( $jo ) ) {
  1752. $ret = "Callback returned no valid JSON !";
  1753. } elsif ( ! $jo->{ok} ) {
  1754. if ( defined( $jo->{description} ) ) {
  1755. $ret = "Callback returned error :".$jo->{description}.":";
  1756. $doRetry = 0 if ($jo->{description} =~ /^Bad Request\:/);
  1757. # Debug "description :".$jo->{description}.":";
  1758. $doRetry = 0 if ($jo->{description} =~ /^Unauthorized/);
  1759. } else {
  1760. $ret = "Callback returned error without description";
  1761. }
  1762. } else {
  1763. if ( defined( $jo->{result} ) ) {
  1764. $result = $jo->{result};
  1765. } else {
  1766. $ret = "Callback returned no result";
  1767. }
  1768. }
  1769. }
  1770. if ( defined( $param->{isPolling} ) ) {
  1771. Log3 $name, 5, "TelegramBot_Callback $name: polling returned result? ".((defined($result))?scalar(@$result):"<undef>");
  1772. # Polling means result must be analyzed
  1773. if ( defined($result) ) {
  1774. # handle result
  1775. $hash->{FAILS} = 0; # succesful UpdatePoll reset fails
  1776. Log3 $name, 5, "UpdatePoll $name: number of results ".scalar(@$result) ;
  1777. foreach my $update ( @$result ) {
  1778. Log3 $name, 5, "UpdatePoll $name: parse result ";
  1779. if ( defined( $update->{message} ) ) {
  1780. $ret = TelegramBot_ParseMsg( $hash, $update->{update_id}, $update->{message} );
  1781. } elsif ( defined( $update->{callback_query} ) ) {
  1782. $ret = TelegramBot_ParseCallbackQuery( $hash, $update->{update_id}, $update->{callback_query} );
  1783. } else {
  1784. Log3 $name, 3, "UpdatePoll $name: inline_query id:".$update->{inline_query}->{id}.
  1785. ": query:".$update->{inline_query}->{query}.":" if ( defined( $update->{inline_query} ) );
  1786. Log3 $name, 3, "UpdatePoll $name: chosen_inline_result id:".$update->{chosen_inline_result}->{result_id}.":".
  1787. " inline id:".$update->{chosen_inline_result}->{inline_message_id}.":".
  1788. " query:".$update->{chosen_inline_result}->{query}.":"
  1789. if ( defined( $update->{chosen_inline_result} ) );
  1790. Log3 $name, 3, "UpdatePoll $name: callback_query id:".$update->{callback_query}->{id}.":".
  1791. " inline id:".$update->{callback_query}->{inline_message_id}.":".
  1792. " data:".$update->{callback_query}->{data}.":"
  1793. if ( defined( $update->{callback_query} ) );
  1794. }
  1795. if ( defined( $ret ) ) {
  1796. last;
  1797. } else {
  1798. $hash->{offset_id} = $update->{update_id}+1;
  1799. }
  1800. }
  1801. }
  1802. # get timestamps and verbose
  1803. my $now = FmtDateTime( gettimeofday() );
  1804. my $tst = ReadingsTimestamp( $name, "PollingErrCount", "1970-01-01 01:00:00" );
  1805. my $pv = AttrVal( $name, "pollingVerbose", "1_Digest" );
  1806. # get current error cnt
  1807. my $cnt = ReadingsVal( $name, "PollingErrCount", "0" );
  1808. # flag if log needs to be written
  1809. my $doLog = 0;
  1810. # Error to be converted to Reading for Poll
  1811. if ( defined( $ret ) ) {
  1812. # something went wrong increase fails
  1813. $hash->{FAILS} += 1;
  1814. # Put last error into reading
  1815. readingsSingleUpdate($hash, "PollingLastError", $ret , 1);
  1816. if ( substr($now,0,10) eq substr($tst,0,10) ) {
  1817. # Still same date just increment
  1818. $cnt += 1;
  1819. readingsSingleUpdate($hash, "PollingErrCount", $cnt, 1);
  1820. } else {
  1821. # Write digest in log on next date
  1822. $doLog = ( $pv ne "3_None" );
  1823. readingsSingleUpdate($hash, "PollingErrCount", 1, 1);
  1824. }
  1825. } elsif ( substr($now,0,10) ne substr($tst,0,10) ) {
  1826. readingsSingleUpdate($hash, "PollingErrCount", 0, 1);
  1827. $doLog = ( $pv ne "3_None" );
  1828. }
  1829. # log level is 2 on error if not digest is selected
  1830. $ll =( ( $pv eq "2_Log" )?2:4 );
  1831. # log digest if flag set
  1832. Log3 $name, 3, "TelegramBot_Callback $name: Digest: Number of poll failures on ".substr($tst,0,10)." is :$cnt:" if ( $doLog );
  1833. # start next poll or wait
  1834. TelegramBot_UpdatePoll($hash);
  1835. } else {
  1836. # Non Polling means: get msgid, reset the params and set loglevel
  1837. $hash->{HU_DO_PARAMS}->{data} = "";
  1838. $ll = 3 if ( defined( $ret ) );
  1839. $msgId = $result->{message_id} if ( ( defined($result) ) && ( ref($result) eq "HASH" ) );
  1840. }
  1841. $ret = "SUCCESS" if ( ! defined( $ret ) );
  1842. Log3 $name, $ll, "TelegramBot_Callback $name: resulted in $ret from ".(( defined( $param->{isPolling} ) )?"Polling":"SendIt");
  1843. if ( ! defined( $param->{isPolling} ) ) {
  1844. $hash->{sentLastResult} = $ret;
  1845. # handle retry
  1846. # ret defined / args defined in params
  1847. if ( ( $ret ne "SUCCESS" ) && ( $doRetry ) && ( defined( $param->{args} ) ) ) {
  1848. my $wait = $param->{args}[$TelegramBot_arg_retrycnt];
  1849. my $maxRetries = AttrVal($name,'maxRetries',0);
  1850. Log3 $name, 4, "TelegramBot_Callback $name: retry count so far $wait (max: $maxRetries) for msg ".
  1851. $param->{args}[0]." : ".$param->{args}[1];
  1852. if ( $wait <= $maxRetries ) {
  1853. # calculate wait time 10s / 100s / 1000s ~ 17min / 10000s ~ 3h / 100000s ~ 30h
  1854. $wait = 10**$wait;
  1855. Log3 $name, 4, "TelegramBot_Callback $name: do retry ".$param->{args}[$TelegramBot_arg_retrycnt]." timer: $wait (ret: $ret) for msg ".
  1856. $param->{args}[0]." : ".$param->{args}[1];
  1857. # set timer
  1858. InternalTimer(gettimeofday()+$wait, "TelegramBot_RetrySend", $param,0);
  1859. # finish
  1860. return;
  1861. }
  1862. Log3 $name, 3, "TelegramBot_Callback $name: Reached max retries (ret: $ret) for msg ".$param->{args}[0]." : ".$param->{args}[1];
  1863. } elsif ( ( $ret ne "SUCCESS" ) && ( ! $doRetry ) ) {
  1864. Log3 $name, 3, "TelegramBot_Callback $name: No retry for (ret: $ret) for msg ".$param->{args}[0]." : ".$param->{args}[1];
  1865. }
  1866. $hash->{sentMsgResult} = $ret;
  1867. $hash->{sentMsgId} = ((defined($msgId))?$msgId:"");
  1868. # Also set sentMsg Id and result in Readings
  1869. readingsBeginUpdate($hash);
  1870. readingsBulkUpdate($hash, "sentMsgResult", $ret);
  1871. readingsBulkUpdate($hash, "sentMsgId", ((defined($msgId))?$msgId:"") );
  1872. readingsBulkUpdate($hash, "sentMsgPeerId", ((defined($hash->{sentMsgPeerId}))?$hash->{sentMsgPeerId}:"") );
  1873. readingsEndUpdate($hash, 1);
  1874. if ( scalar( @{ $hash->{sentQueue} } ) ) {
  1875. my $ref = shift @{ $hash->{sentQueue} };
  1876. Log3 $name, 5, "TelegramBot_Callback $name: handle queued send with :@$ref[0]: -:@$ref[1]: ";
  1877. TelegramBot_SendIt( $hash, @$ref[0], @$ref[1], @$ref[2], @$ref[3], @$ref[4], @$ref[5], @$ref[6] );
  1878. }
  1879. }
  1880. Log3 $name, 5, "TelegramBot_Callback $name: - Ende > Control back to FHEM";
  1881. }
  1882. #####################################
  1883. # INTERNAL: _ParseMsg handle a message from the update call
  1884. # params are the hash, the updateid and the actual message
  1885. sub TelegramBot_ParseMsg($$$)
  1886. {
  1887. my ( $hash, $uid, $message ) = @_;
  1888. my $name = $hash->{NAME};
  1889. my @contacts;
  1890. my $ret;
  1891. my $mid = $message->{message_id};
  1892. my $from = $message->{from};
  1893. my $mpeer = $from->{id};
  1894. # ignore if unknown contacts shall be accepter
  1895. if ( ( AttrVal($name,'allowUnknownContacts',1) == 0 ) && ( ! TelegramBot_IsKnownContact( $hash, $mpeer ) ) ) {
  1896. my $mName = $from->{first_name};
  1897. $mName .= " ".$from->{last_name} if ( defined($from->{last_name}) );
  1898. Log3 $name, 3, "TelegramBot $name: Message from unknown Contact (id:$mpeer: name:$mName:) blocked";
  1899. return $ret;
  1900. }
  1901. # check peers beside from only contact (shared contact) and new_chat_participant are checked
  1902. push( @contacts, $from );
  1903. my $chatId = "";
  1904. my $chat = $message->{chat};
  1905. if ( ( defined( $chat ) ) && ( $chat->{type} ne "private" ) ) {
  1906. push( @contacts, $chat );
  1907. $chatId = $chat->{id};
  1908. }
  1909. my $user = $message->{new_chat_participant};
  1910. if ( defined( $user ) ) {
  1911. push( @contacts, $user );
  1912. }
  1913. # get reply message id
  1914. my $replyId;
  1915. my $replyPart = $message->{reply_to_message};
  1916. if ( defined( $replyPart ) ) {
  1917. $replyId = $replyPart->{message_id};
  1918. }
  1919. # mtext contains the text of the message (if empty no further handling)
  1920. my ( $mtext, $mfileid );
  1921. if ( defined( $message->{text} ) ) {
  1922. # handle text message
  1923. $mtext = $message->{text};
  1924. Log3 $name, 4, "TelegramBot_ParseMsg $name: Textmessage";
  1925. } elsif ( defined( $message->{audio} ) ) {
  1926. # handle audio message
  1927. my $subtype = $message->{audio};
  1928. $mtext = "received audio ";
  1929. $mfileid = $subtype->{file_id};
  1930. $mtext .= " # Performer: ".$subtype->{performer} if ( defined( $subtype->{performer} ) );
  1931. $mtext .= " # Title: ".$subtype->{title} if ( defined( $subtype->{title} ) );
  1932. $mtext .= " # Mime: ".$subtype->{mime_type} if ( defined( $subtype->{mime_type} ) );
  1933. $mtext .= " # Size: ".$subtype->{file_size} if ( defined( $subtype->{file_size} ) );
  1934. Log3 $name, 4, "TelegramBot_ParseMsg $name: audio fileid: $mfileid";
  1935. } elsif ( defined( $message->{document} ) ) {
  1936. # handle document message
  1937. my $subtype = $message->{document};
  1938. $mtext = "received document ";
  1939. $mfileid = $subtype->{file_id};
  1940. $mtext .= " # Caption: ".$message->{caption} if ( defined( $message->{caption} ) );
  1941. $mtext .= " # Name: ".$subtype->{file_name} if ( defined( $subtype->{file_name} ) );
  1942. $mtext .= " # Mime: ".$subtype->{mime_type} if ( defined( $subtype->{mime_type} ) );
  1943. $mtext .= " # Size: ".$subtype->{file_size} if ( defined( $subtype->{file_size} ) );
  1944. Log3 $name, 4, "TelegramBot_ParseMsg $name: document fileid: $mfileid ";
  1945. } elsif ( defined( $message->{voice} ) ) {
  1946. # handle voice message
  1947. my $subtype = $message->{voice};
  1948. $mtext = "received voice ";
  1949. $mfileid = $subtype->{file_id};
  1950. $mtext .= " # Mime: ".$subtype->{mime_type} if ( defined( $subtype->{mime_type} ) );
  1951. $mtext .= " # Size: ".$subtype->{file_size} if ( defined( $subtype->{file_size} ) );
  1952. Log3 $name, 4, "TelegramBot_ParseMsg $name: voice fileid: $mfileid";
  1953. } elsif ( defined( $message->{video} ) ) {
  1954. # handle video message
  1955. my $subtype = $message->{video};
  1956. $mtext = "received video ";
  1957. $mfileid = $subtype->{file_id};
  1958. $mtext .= " # Caption: ".$message->{caption} if ( defined( $message->{caption} ) );
  1959. $mtext .= " # Mime: ".$subtype->{mime_type} if ( defined( $subtype->{mime_type} ) );
  1960. $mtext .= " # Size: ".$subtype->{file_size} if ( defined( $subtype->{file_size} ) );
  1961. Log3 $name, 4, "TelegramBot_ParseMsg $name: video fileid: $mfileid";
  1962. } elsif ( defined( $message->{photo} ) ) {
  1963. # handle photo message
  1964. # photos are always an array with (hopefully) the biggest size last in the array
  1965. my $photolist = $message->{photo};
  1966. if ( scalar(@$photolist) > 0 ) {
  1967. my $subtype = $$photolist[scalar(@$photolist)-1] ;
  1968. $mtext = "received photo ";
  1969. $mfileid = $subtype->{file_id};
  1970. $mtext .= " # Caption: ".$message->{caption} if ( defined( $message->{caption} ) );
  1971. $mtext .= " # Mime: ".$subtype->{mime_type} if ( defined( $subtype->{mime_type} ) );
  1972. $mtext .= " # Size: ".$subtype->{file_size} if ( defined( $subtype->{file_size} ) );
  1973. Log3 $name, 4, "TelegramBot_ParseMsg $name: photo fileid: $mfileid";
  1974. }
  1975. } elsif ( defined( $message->{venue} ) ) {
  1976. # handle location type message
  1977. my $ven = $message->{venue};
  1978. my $loc = $ven->{location};
  1979. $mtext = "received venue ";
  1980. $mtext .= " # latitude: ".$loc->{latitude}." # longitude: ".$loc->{longitude};
  1981. $mtext .= " # title: ".$ven->{title}." # address: ".$ven->{address};
  1982. # urls will be discarded in fhemweb $mtext .= "\n# url: <a href=\"http://maps.google.com/?q=loc:".$loc->{latitude}.",".$loc->{longitude}."\">maplink</a>";
  1983. Log3 $name, 4, "TelegramBot_ParseMsg $name: location received: latitude: ".$loc->{latitude}." longitude: ".$loc->{longitude};;
  1984. } elsif ( defined( $message->{location} ) ) {
  1985. # handle location type message
  1986. my $loc = $message->{location};
  1987. $mtext = "received location ";
  1988. $mtext .= " # latitude: ".$loc->{latitude}." # longitude: ".$loc->{longitude};
  1989. # urls will be discarded in fhemweb $mtext .= "\n# url: <a href=\"http://maps.google.com/?q=loc:".$loc->{latitude}.",".$loc->{longitude}."\">maplink</a>";
  1990. Log3 $name, 4, "TelegramBot_ParseMsg $name: location received: latitude: ".$loc->{latitude}." longitude: ".$loc->{longitude};;
  1991. }
  1992. if ( defined( $mtext ) ) {
  1993. Log3 $name, 4, "TelegramBot_ParseMsg $name: text :$mtext:";
  1994. my $mpeernorm = $mpeer;
  1995. $mpeernorm =~ s/^\s+|\s+$//g;
  1996. $mpeernorm =~ s/ /_/g;
  1997. my $mchatnorm = "";
  1998. $mchatnorm = $chatId if ( AttrVal($name,'cmdRespondChat',1) == 1 );
  1999. # Log3 $name, 5, "TelegramBot_ParseMsg $name: Found message $mid from $mpeer :$mtext:";
  2000. # contacts handled separately since readings are updated in here
  2001. TelegramBot_ContactUpdate($hash, @contacts) if ( scalar(@contacts) > 0 );
  2002. readingsBeginUpdate($hash);
  2003. readingsBulkUpdate($hash, "prevMsgId", $hash->{READINGS}{msgId}{VAL});
  2004. readingsBulkUpdate($hash, "prevMsgPeer", $hash->{READINGS}{msgPeer}{VAL});
  2005. readingsBulkUpdate($hash, "prevMsgPeerId", $hash->{READINGS}{msgPeerId}{VAL});
  2006. readingsBulkUpdate($hash, "prevMsgChat", $hash->{READINGS}{msgChat}{VAL});
  2007. readingsBulkUpdate($hash, "prevMsgText", $hash->{READINGS}{msgText}{VAL});
  2008. readingsBulkUpdate($hash, "prevMsgFileId", $hash->{READINGS}{msgFileId}{VAL});
  2009. readingsBulkUpdate($hash, "prevMsgReplyMsgId", $hash->{READINGS}{msgReplyMsgId}{VAL});
  2010. readingsEndUpdate($hash, 0);
  2011. readingsBeginUpdate($hash);
  2012. readingsBulkUpdate($hash, "msgId", $mid);
  2013. readingsBulkUpdate($hash, "msgPeer", TelegramBot_GetFullnameForContact( $hash, $mpeernorm ));
  2014. readingsBulkUpdate($hash, "msgPeerId", $mpeernorm);
  2015. readingsBulkUpdate($hash, "msgChat", TelegramBot_GetFullnameForContact( $hash, ((!$chatId)?$mpeernorm:$chatId) ) );
  2016. readingsBulkUpdate($hash, "msgChatId", ((!$chatId)?$mpeernorm:$chatId) );
  2017. readingsBulkUpdate($hash, "msgText", $mtext);
  2018. readingsBulkUpdate($hash, "msgReplyMsgId", (defined($replyId)?$replyId:""));
  2019. readingsBulkUpdate($hash, "msgFileId", ( ( defined( $mfileid ) ) ? $mfileid : "" ) );
  2020. readingsEndUpdate($hash, 1);
  2021. # COMMAND Handling (only if no fileid found
  2022. Telegram_HandleCommandInMessages( $hash, $mpeernorm, $mchatnorm, $mtext, undef, 0 ) if ( ! defined( $mfileid ) );
  2023. } elsif ( scalar(@contacts) > 0 ) {
  2024. # will also update reading
  2025. TelegramBot_ContactUpdate( $hash, @contacts );
  2026. Log3 $name, 5, "TelegramBot_ParseMsg $name: Found message $mid from $mpeer without text/media but with contacts";
  2027. } else {
  2028. Log3 $name, 5, "TelegramBot_ParseMsg $name: Found message $mid from $mpeer without text/media";
  2029. }
  2030. return $ret;
  2031. }
  2032. #####################################
  2033. # INTERNAL: _ParseCallbackQuery handle the callback of a query provide as
  2034. # params are the hash, the updateid and the actual message
  2035. sub TelegramBot_ParseCallbackQuery($$$)
  2036. {
  2037. my ( $hash, $uid, $callback ) = @_;
  2038. my $name = $hash->{NAME};
  2039. my @contacts;
  2040. my $ret;
  2041. my $qid = $callback->{id};
  2042. my $from = $callback->{from};
  2043. my $mpeer = $from->{id};
  2044. # get reply message id
  2045. my $replyId;
  2046. my $chatId;
  2047. my $replyPart = $callback->{message};
  2048. if ( defined( $replyPart ) ) {
  2049. $replyId = $replyPart->{message_id};
  2050. my $chat = $replyPart->{chat};
  2051. if ( ( defined( $chat ) ) && ( $chat->{type} ne "private" ) ) {
  2052. $chatId = $chat->{id};
  2053. }
  2054. }
  2055. my $imid = $callback->{inline_message_id};
  2056. my $chat= $callback->{chat_instance};
  2057. # Debug "Chat :".$chat.":";
  2058. my $data = $callback->{data};
  2059. my $mtext = "Callback for inline query id: $qid from : $mpeer : data : ".(defined($data)?$data:"<undef>");
  2060. # ignore if unknown contacts shall be accepter
  2061. if ( ( AttrVal($name,'allowUnknownContacts',1) == 0 ) && ( ! TelegramBot_IsKnownContact( $hash, $mpeer ) ) ) {
  2062. my $mName = $from->{first_name};
  2063. $mName .= " ".$from->{last_name} if ( defined($from->{last_name}) );
  2064. Log3 $name, 3, "TelegramBot $name: Message from unknown Contact (id:$mpeer: name:$mName:) blocked";
  2065. return $ret;
  2066. }
  2067. my $mpeernorm = $mpeer;
  2068. $mpeernorm =~ s/^\s+|\s+$//g;
  2069. $mpeernorm =~ s/ /_/g;
  2070. # check peers beside from only contact (shared contact) and new_chat_participant are checked
  2071. push( @contacts, $from );
  2072. my $answerData = "";
  2073. if ( TelegramBot_checkAllowedPeer( $hash, $mpeernorm, $mtext ) ) {
  2074. Log3 $name, 4, "TelegramBot_ParseCallback $name: ".$mtext;
  2075. # contacts handled separately since readings are updated in here
  2076. TelegramBot_ContactUpdate($hash, @contacts) if ( scalar(@contacts) > 0 );
  2077. readingsBeginUpdate($hash);
  2078. readingsBulkUpdate($hash, "queryID", $qid);
  2079. readingsBulkUpdate($hash, "queryPeer", TelegramBot_GetFullnameForContact( $hash, $mpeernorm ));
  2080. readingsBulkUpdate($hash, "queryPeerId", $mpeernorm);
  2081. readingsBulkUpdate($hash, "queryData", ( ( defined( $data ) ) ? $data : "" ) );
  2082. readingsBulkUpdate($hash, "queryReplyMsgId", ( ( defined( $replyId ) ) ? $replyId : "" ) );
  2083. readingsEndUpdate($hash, 1);
  2084. $answerData = AttrVal($name,'queryAnswerText',undef);
  2085. # special handling for TBot_List
  2086. if ( defined( $data ) ) {
  2087. if ( $data =~ /^TBL_.+/ ) {
  2088. # TBot_List has always prefix "TBL_"
  2089. my @ltbots = devspec2array( "TYPE=TBot_List" );
  2090. my $ltbotresult;
  2091. foreach my $ltbot ( @ltbots ) {
  2092. $ltbotresult = fhem( "get $ltbot queryAnswer $name $mpeernorm $data", 1 );
  2093. if ( defined( $ltbotresult ) ) {
  2094. $answerData = $ltbotresult;
  2095. last;
  2096. }
  2097. }
  2098. } elsif ( $data =~ /^TBOT_FAVORITE_(.+)$/ ) {
  2099. # Telegrambot has always prefix "TBOT_"
  2100. # ??? favorites inline
  2101. my $favpart = $1;
  2102. my $fcmd = AttrVal($name,'cmdFavorites',undef);
  2103. if ( $fcmd ) {
  2104. if ( $favpart =~ /^-[0-9]+$/ ) {
  2105. $fcmd .= " $favpart-";
  2106. } elsif ( $favpart =~ /^[0-9]+$/ ) {
  2107. $fcmd .= " $favpart";
  2108. } elsif ( $favpart =~ /^MENU$/ ) {
  2109. } else {
  2110. # assume cancel in all other cases
  2111. $fcmd .= " cancel";
  2112. }
  2113. # where to get chat id from ?
  2114. Log3 $name, 4, "TelegramBot_ParseCallback $name: REPLYMID: $replyId";
  2115. Telegram_HandleCommandInMessages( $hash, $mpeernorm, $chatId, $fcmd, $replyId, 1 );
  2116. }
  2117. }
  2118. }
  2119. }
  2120. # sent answer if not undef
  2121. if ( defined( $answerData ) ) {
  2122. $answerData = "" if ( ! $answerData );
  2123. if ( length( $answerData ) > 0 ) {
  2124. my %dummy;
  2125. my ($err, @a) = ReplaceSetMagic(\%dummy, 0, ( $answerData ) );
  2126. if ( $err ) {
  2127. Log3 $name, 1, "TelegramBot_ParseCallback $name: parse answerData failed on ReplaceSetmagic with :$err: on :$answerData:";
  2128. $answerData = "";
  2129. } else {
  2130. $answerData = join(" ", @a);
  2131. Log3 $name, 4, "TelegramBot_ParseCallback $name: parse answerData returned :$answerData:";
  2132. }
  2133. }
  2134. my $tmpRet = TelegramBot_SendIt( $hash, $mpeernorm, $answerData, $qid, 12, undef );
  2135. Log3 $name, 1, "TelegramBot_ParseCallback $name: send answer failed with :$tmpRet: " if ( $tmpRet );
  2136. }
  2137. return $ret;
  2138. }
  2139. ##############################################################################
  2140. ##############################################################################
  2141. ##
  2142. ## Polling / Setup
  2143. ##
  2144. ##############################################################################
  2145. ##############################################################################
  2146. ######################################
  2147. # make sure a reinitialization is triggered on next update
  2148. #
  2149. sub TelegramBot_ResetPolling($) {
  2150. my ($hash) = @_;
  2151. my $name = $hash->{NAME};
  2152. Log3 $name, 4, "TelegramBot_ResetPolling $name: called ";
  2153. RemoveInternalTimer($hash);
  2154. HttpUtils_Close( $hash->{HU_UPD_PARAMS} );
  2155. HttpUtils_Close( $hash->{HU_DO_PARAMS} );
  2156. $hash->{WAIT} = 0;
  2157. $hash->{FAILS} = 0;
  2158. # let all existing methods first run into block
  2159. $hash->{POLLING} = -1;
  2160. # wait some time before next polling is starting
  2161. InternalTimer(gettimeofday()+30, "TelegramBot_RestartPolling", $hash,0);
  2162. Log3 $name, 4, "TelegramBot_ResetPolling $name: finished ";
  2163. }
  2164. ######################################
  2165. # make sure a reinitialization is triggered on next update
  2166. #
  2167. sub TelegramBot_RestartPolling($) {
  2168. my ($hash) = @_;
  2169. my $name = $hash->{NAME};
  2170. Log3 $name, 4, "TelegramBot_RestartPolling $name: called ";
  2171. # Now polling can start
  2172. $hash->{POLLING} = 0;
  2173. # wait some time before next polling is starting
  2174. TelegramBot_UpdatePoll($hash);
  2175. Log3 $name, 4, "TelegramBot_RestartPolling $name: finished ";
  2176. }
  2177. ######################################
  2178. # make sure a reinitialization is triggered on next update
  2179. #
  2180. sub TelegramBot_Setup($) {
  2181. my ($hash) = @_;
  2182. my $name = $hash->{NAME};
  2183. Log3 $name, 4, "TelegramBot_Setup $name: called ";
  2184. $hash->{me} = "<unknown>";
  2185. $hash->{STATE} = "Undefined";
  2186. $hash->{POLLING} = -1;
  2187. $hash->{HU_UPD_PARAMS}->{callback} = \&TelegramBot_Callback;
  2188. $hash->{HU_DO_PARAMS}->{callback} = \&TelegramBot_Callback;
  2189. # Temp?? SNAME is required for allowed (normally set in TCPServerUtils)
  2190. $hash->{SNAME} = $name;
  2191. # Ensure queueing is not happening
  2192. delete( $hash->{sentQueue} );
  2193. delete( $hash->{sentMsgResult} );
  2194. # remove timer for retry
  2195. RemoveInternalTimer($hash->{HU_DO_PARAMS});
  2196. $hash->{STATE} = "Defined";
  2197. if ( ! TelegramBot_readToken($hash) ) {
  2198. Log3 $name, 1, "TelegramBot_Setup $name: no valid API token found, please call \"set $name token <token>\" (once)";
  2199. $hash->{STATE} = "NoValidToken";
  2200. } else {
  2201. # getMe as connectivity check and set internals accordingly
  2202. my $url = TelegramBot_getBaseURL($hash)."getMe";
  2203. my $meret = TelegramBot_DoUrlCommand( $hash, $url, 1 ); # ignore first error
  2204. if ( ( ! defined($meret) ) || ( ref($meret) ne "HASH" ) ) {
  2205. # retry on first failure
  2206. $meret = TelegramBot_DoUrlCommand( $hash, $url );
  2207. }
  2208. if ( ( defined($meret) ) && ( ref($meret) eq "HASH" ) ) {
  2209. $hash->{me} = TelegramBot_userObjectToString( $meret );
  2210. $hash->{STATE} = "Setup";
  2211. } else {
  2212. $hash->{me} = "Failed - see log file for details";
  2213. $hash->{STATE} = "Failed";
  2214. $hash->{FAILS} = 1;
  2215. }
  2216. }
  2217. my %hh = ();
  2218. $hash->{inlinechats} = \%hh;
  2219. TelegramBot_InternalContactsFromReading( $hash);
  2220. TelegramBot_ResetPolling($hash);
  2221. Log3 $name, 4, "TelegramBot_Setup $name: ended ";
  2222. }
  2223. ##############################################################################
  2224. ##############################################################################
  2225. ##
  2226. ## CONTACT handling
  2227. ##
  2228. ##############################################################################
  2229. ##############################################################################
  2230. #####################################
  2231. # INTERNAL: get id for a peer
  2232. # if only digits --> assume id
  2233. # if start with @ --> assume username
  2234. # if start with # --> assume groupname
  2235. # else --> assume full name
  2236. sub TelegramBot_GetIdForPeer($$)
  2237. {
  2238. my ($hash,$mpeer) = @_;
  2239. TelegramBot_InternalContactsFromReading( $hash ) if ( ! defined( $hash->{Contacts} ) );
  2240. my $id;
  2241. if ( $mpeer =~ /^\-?[[:digit:]]+$/ ) {
  2242. # check if id is in hash
  2243. # $id = $mpeer if ( defined( $hash->{Contacts}{$mpeer} ) );
  2244. # Allow also sending to ids which are not in the contacts list
  2245. $id = $mpeer;
  2246. } elsif ( $mpeer =~ /^[@#].*$/ ) {
  2247. foreach my $mkey ( keys %{$hash->{Contacts}} ) {
  2248. my @clist = split( /:/, $hash->{Contacts}{$mkey} );
  2249. if ( (defined($clist[2])) && ( $clist[2] eq $mpeer ) ) {
  2250. $id = $clist[0];
  2251. last;
  2252. }
  2253. }
  2254. } else {
  2255. $mpeer =~ s/^\s+|\s+$//g;
  2256. $mpeer =~ s/ /_/g;
  2257. foreach my $mkey ( keys %{$hash->{Contacts}} ) {
  2258. my @clist = split( /:/, $hash->{Contacts}{$mkey} );
  2259. if ( (defined($clist[1])) && ( $clist[1] eq $mpeer ) ) {
  2260. $id = $clist[0];
  2261. last;
  2262. }
  2263. }
  2264. }
  2265. return $id
  2266. }
  2267. #####################################
  2268. # INTERNAL: get full name for contact id
  2269. sub TelegramBot_GetContactInfoForContact($$)
  2270. {
  2271. my ($hash,$mcid) = @_;
  2272. TelegramBot_InternalContactsFromReading( $hash ) if ( ! defined( $hash->{Contacts} ) );
  2273. return ( $hash->{Contacts}{$mcid});
  2274. }
  2275. #####################################
  2276. # INTERNAL: get full name for contact id
  2277. sub TelegramBot_GetFullnameForContact($$)
  2278. {
  2279. my ($hash,$mcid) = @_;
  2280. my $contact = TelegramBot_GetContactInfoForContact( $hash,$mcid );
  2281. my $ret = "";
  2282. if ( defined( $contact ) ) {
  2283. Log3 $hash->{NAME}, 4, "TelegramBot_GetFullnameForContact # Contacts is $contact:";
  2284. my @clist = split( /:/, $contact );
  2285. $ret = $clist[1];
  2286. $ret = $clist[2] if ( ! $ret);
  2287. $ret = $clist[0] if ( ! $ret);
  2288. Log3 $hash->{NAME}, 4, "TelegramBot_GetFullnameForContact # name is $ret";
  2289. } else {
  2290. Log3 $hash->{NAME}, 4, "TelegramBot_GetFullnameForContact # Contacts is <undef>";
  2291. }
  2292. return $ret;
  2293. }
  2294. #####################################
  2295. # INTERNAL: get full name for a chat
  2296. sub TelegramBot_GetFullnameForChat($$)
  2297. {
  2298. my ($hash,$mcid) = @_;
  2299. my $ret = "";
  2300. return $ret if ( ! $mcid );
  2301. my $contact = TelegramBot_GetContactInfoForContact( $hash,$mcid );
  2302. if ( defined( $contact ) ) {
  2303. my @clist = split( /:/, $contact );
  2304. $ret = $clist[0];
  2305. $ret .= " (".$clist[2].")" if ( $clist[2] );
  2306. Log3 $hash->{NAME}, 4, "TelegramBot_GetFullnameForChat # $mcid is $ret";
  2307. }
  2308. return $ret;
  2309. }
  2310. #####################################
  2311. # INTERNAL: check if a contact is already known in the internals->Contacts-hash
  2312. sub TelegramBot_IsKnownContact($$)
  2313. {
  2314. my ($hash,$mpeer) = @_;
  2315. TelegramBot_InternalContactsFromReading( $hash ) if ( ! defined( $hash->{Contacts} ) );
  2316. # foreach my $key (keys $hash->{Contacts} )
  2317. # {
  2318. # Log3 $hash->{NAME}, 4, "Contact :$key: is :".$hash->{Contacts}{$key}.":";
  2319. # }
  2320. # Debug "Is known ? ".( defined( $hash->{Contacts}{$mpeer} ) );
  2321. return ( defined( $hash->{Contacts}{$mpeer} ) );
  2322. }
  2323. #####################################
  2324. # INTERNAL: calculate internals->contacts-hash from Readings->Contacts string
  2325. sub TelegramBot_CalcContactsHash($$)
  2326. {
  2327. my ($hash, $cstr) = @_;
  2328. # create a new hash
  2329. if ( defined( $hash->{Contacts} ) ) {
  2330. foreach my $key (keys %{$hash->{Contacts}} )
  2331. {
  2332. delete $hash->{Contacts}{$key};
  2333. }
  2334. } else {
  2335. $hash->{Contacts} = {};
  2336. }
  2337. # split reading at separator
  2338. my @contactList = split(/\s+/, $cstr );
  2339. # for each element - get id as hashtag and full contact as value
  2340. foreach my $contact ( @contactList ) {
  2341. my ( $id, $cname, $cuser ) = split( ":", $contact, 3 );
  2342. # add contact only if all three parts are there and either 2nd or 3rd part not empty and 3rd part either empty or start with @ or # and at least 3 chars
  2343. # and id must be only digits
  2344. $cuser = "" if ( ! defined( $cuser ) );
  2345. $cname = "" if ( ! defined( $cname ) );
  2346. Log3 $hash->{NAME}, 5, "Contact add :$contact: :$id: :$cname: :$cuser:";
  2347. if ( ( length( $cname ) == 0 ) && ( length( $cuser ) == 0 ) ) {
  2348. Log3 $hash->{NAME}, 5, "Contact add :$contact: has empty cname and cuser:";
  2349. next;
  2350. } elsif ( ( length( $cuser ) > 0 ) && ( length( $cuser ) < 3 ) ) {
  2351. Log3 $hash->{NAME}, 5, "Contact add :$contact: cuser not long enough (3):";
  2352. next;
  2353. } elsif ( ( length( $cuser ) > 0 ) && ( $cuser !~ /^[\@#]/ ) ) {
  2354. Log3 $hash->{NAME}, 5, "Contact add :$contact: cuser not matching start chars:";
  2355. next;
  2356. } elsif ( $id !~ /^\-?[[:digit:]]+$/ ) {
  2357. Log3 $hash->{NAME}, 5, "Contact add :$contact: cid is not number or -number:";
  2358. next;
  2359. } else {
  2360. $cname = TelegramBot_encodeContactString( $cname );
  2361. $cuser = TelegramBot_encodeContactString( $cuser );
  2362. $hash->{Contacts}{$id} = $id.":".$cname.":".$cuser;
  2363. }
  2364. }
  2365. }
  2366. #####################################
  2367. # INTERNAL: calculate internals->contacts-hash from Readings->Contacts string
  2368. sub TelegramBot_InternalContactsFromReading($)
  2369. {
  2370. my ($hash) = @_;
  2371. TelegramBot_CalcContactsHash( $hash, ReadingsVal($hash->{NAME},"Contacts","") );
  2372. }
  2373. #####################################
  2374. # INTERNAL: update contacts hash and change readings string (no return)
  2375. sub TelegramBot_ContactUpdate($@) {
  2376. my ($hash, @contacts) = @_;
  2377. my $newfound = ( int(@contacts) == 0 );
  2378. my $oldContactString = ReadingsVal($hash->{NAME},"Contacts","");
  2379. TelegramBot_InternalContactsFromReading( $hash ) if ( ! defined( $hash->{Contacts} ) );
  2380. Log3 $hash->{NAME}, 4, "TelegramBot_ContactUpdate # Contacts in hash before :".scalar(keys %{$hash->{Contacts}}).":";
  2381. foreach my $user ( @contacts ) {
  2382. my $contactString = TelegramBot_userObjectToString( $user );
  2383. # keep the username part of the new contatc for deleting old users with same username
  2384. my $unamepart;
  2385. my @clist = split( /:/, $contactString );
  2386. if (defined($clist[2])) {
  2387. $unamepart = $clist[2];
  2388. }
  2389. if ( ! defined( $hash->{Contacts}{$user->{id}} ) ) {
  2390. Log3 $hash->{NAME}, 3, "TelegramBot_ContactUpdate new contact :".$contactString.":";
  2391. next if ( AttrVal($hash->{NAME},'allowUnknownContacts',1) == 0 );
  2392. $newfound = 1;
  2393. } elsif ( $contactString ne $hash->{Contacts}{$user->{id}} ) {
  2394. Log3 $hash->{NAME}, 3, "TelegramBot_ContactUpdate updated contact :".$contactString.":";
  2395. }
  2396. # remove all contacts with same username
  2397. if ( defined( $unamepart ) ) {
  2398. my $dupid = TelegramBot_GetIdForPeer( $hash, $unamepart );
  2399. while ( $dupid ) {
  2400. Log3 $hash->{NAME}, 3, "TelegramBot_ContactUpdate removed stale/duplicate contact ($dupid:$unamepart):".$hash->{Contacts}{$dupid}.":" if ( $dupid ne $user->{id} );
  2401. delete( $hash->{Contacts}{$dupid} );
  2402. $dupid = TelegramBot_GetIdForPeer( $hash, $unamepart );
  2403. }
  2404. }
  2405. # set new contact data
  2406. $hash->{Contacts}{$user->{id}} = $contactString;
  2407. }
  2408. Log3 $hash->{NAME}, 4, "TelegramBot_ContactUpdate # Contacts in hash after :".scalar(keys %{$hash->{Contacts}}).":";
  2409. my $rc = "";
  2410. foreach my $key ( keys %{$hash->{Contacts}} )
  2411. {
  2412. if ( length($rc) > 0 ) {
  2413. $rc .= " ".$hash->{Contacts}{$key};
  2414. } else {
  2415. $rc = $hash->{Contacts}{$key};
  2416. }
  2417. }
  2418. # Do a readings change directly for contacts
  2419. readingsSingleUpdate($hash, "Contacts", $rc , 1) if ( $rc ne $oldContactString );
  2420. # save state file on new contact
  2421. if ( $newfound ) {
  2422. WriteStatefile() if ( AttrVal($hash->{NAME}, 'saveStateOnContactChange', 1) ) ;
  2423. Log3 $hash->{NAME}, 2, "TelegramBot_ContactUpdate Updated Contact list :".$rc.":";
  2424. }
  2425. return;
  2426. }
  2427. #####################################
  2428. # INTERNAL: Convert TelegramBot user and chat object to string
  2429. sub TelegramBot_userObjectToString($) {
  2430. my ( $user ) = @_;
  2431. my $ret = $user->{id}.":";
  2432. # user objects do not contain a type field / chat objects need to contain a type but only if type=group or type=supergroup it is really a group
  2433. if ( ( defined( $user->{type} ) ) && ( ( $user->{type} eq "group" ) || ( $user->{type} eq "supergroup" ) ) ) {
  2434. $ret .= ":";
  2435. $ret .= "#".TelegramBot_encodeContactString($user->{title}) if ( defined( $user->{title} ) );
  2436. } else {
  2437. my $part = "";
  2438. $part .= $user->{first_name} if ( defined( $user->{first_name} ) );
  2439. $part .= " ".$user->{last_name} if ( defined( $user->{last_name} ) );
  2440. $ret .= TelegramBot_encodeContactString($part).":";
  2441. $ret .= "@".TelegramBot_encodeContactString($user->{username}) if ( defined( $user->{username} ) );
  2442. }
  2443. return $ret;
  2444. }
  2445. #####################################
  2446. # INTERNAL: Convert TelegramBot user and chat object to string
  2447. sub TelegramBot_encodeContactString($) {
  2448. my ($str) = @_;
  2449. $str =~ s/:/_/g;
  2450. $str =~ s/^\s+|\s+$//g;
  2451. $str =~ s/ /_/g;
  2452. return $str;
  2453. }
  2454. #####################################
  2455. # INTERNAL: Check if peer is allowed - true if allowed
  2456. sub TelegramBot_checkAllowedPeer($$$) {
  2457. my ($hash,$mpeer,$msg) = @_;
  2458. my $name = $hash->{NAME};
  2459. Log3 $name, 5, "TelegramBot_checkAllowedPeer $name: called with $mpeer";
  2460. my $cp = AttrVal($name,'cmdRestrictedPeer','');
  2461. return 1 if ( $cp eq '' );
  2462. my @peers = split( " ", $cp);
  2463. foreach my $cp (@peers) {
  2464. return 1 if ( $cp eq $mpeer );
  2465. my $cdefpeer = TelegramBot_GetIdForPeer( $hash, $cp );
  2466. if ( defined( $cdefpeer ) ) {
  2467. return 1 if ( $cdefpeer eq $mpeer );
  2468. }
  2469. }
  2470. # get human readble name for peer
  2471. my $pname = TelegramBot_GetFullnameForContact( $hash, $mpeer );
  2472. # unauthorized fhem cmd
  2473. Log3 $name, 1, "TelegramBot unauthorized cmd from user :$pname: ($mpeer) \n Msg: $msg";
  2474. # LOCAL: External message
  2475. my $ret = AttrVal( $name, 'textResponseUnauthorized', 'UNAUTHORIZED: TelegramBot FHEM request from user :$peer \n Msg: $msg');
  2476. $ret =~ s/\$peer/$pname ($mpeer)/g;
  2477. $ret =~ s/\$msg/$msg/g;
  2478. # my $ret = "UNAUTHORIZED: TelegramBot FHEM request from user :$pname: ($mpeer) \n Msg: $msg";
  2479. # send unauthorized to defaultpeer
  2480. my $defpeer = AttrVal($name,'defaultPeer',undef);
  2481. if ( defined( $defpeer ) ) {
  2482. AnalyzeCommand( undef, "set $name message $ret" );
  2483. }
  2484. return 0;
  2485. }
  2486. ##############################################################################
  2487. ##############################################################################
  2488. ##
  2489. ## HELPER
  2490. ##
  2491. ##############################################################################
  2492. ##############################################################################
  2493. #####################################
  2494. # stores Telegram API Token
  2495. sub TelegramBot_getBaseURL($)
  2496. {
  2497. my ($hash) = @_;
  2498. my $token = TelegramBot_readToken( $hash );
  2499. # Debug "Token ".$hash->{NAME}." ".$token;
  2500. return "https://api.telegram.org/bot".$token."/";
  2501. }
  2502. #####################################
  2503. # stores Telegram API Token
  2504. sub TelegramBot_storeToken($$;$)
  2505. {
  2506. my ($hash, $token, $name) = @_;
  2507. if ( $token !~ /^([[:alnum:]]|[-:_])+[[:alnum:]]+([[:alnum:]]|[-:_])+$/ ) {
  2508. return "specify valid API token containing only alphanumeric characters and -: characters";
  2509. }
  2510. $name = $hash->{NAME} if ( ! defined($name) );
  2511. my $index = "TelegramBot_".$name."_token";
  2512. my $key = getUniqueId().$index;
  2513. my $enc_pwd = "";
  2514. if(eval "use Digest::MD5;1")
  2515. {
  2516. $key = Digest::MD5::md5_hex(unpack "H*", $key);
  2517. $key .= Digest::MD5::md5_hex($key);
  2518. }
  2519. for my $char (split //, $token)
  2520. {
  2521. my $encode=chop($key);
  2522. $enc_pwd.=sprintf("%.2x",ord($char)^ord($encode));
  2523. $key=$encode.$key;
  2524. }
  2525. my $err = setKeyValue($index, $enc_pwd);
  2526. return "error while saving the API token - $err" if(defined($err));
  2527. return "API token successfully saved";
  2528. } # end storeToken
  2529. #####################################
  2530. # reads the Telegram API Token
  2531. sub TelegramBot_readToken($;$)
  2532. {
  2533. my ($hash, $name) = @_;
  2534. $name = $hash->{NAME} if ( ! defined($name) );
  2535. my $index = "TelegramBot_" . $name . "_token";
  2536. my $key = getUniqueId().$index;
  2537. my ($token, $err);
  2538. Log3 $hash, 5, "TelegramBot_readToken: Read Telegram API token from file";
  2539. ($err, $token) = getKeyValue($index);
  2540. if ( defined($err) ) {
  2541. Log3 $hash, 1, "TelegramBot_readToken: Error: unable to read API token from file: $err";
  2542. return "";
  2543. }
  2544. if ( defined($token) ) {
  2545. if ( eval "use Digest::MD5;1" ) {
  2546. $key = Digest::MD5::md5_hex(unpack "H*", $key);
  2547. $key .= Digest::MD5::md5_hex($key);
  2548. }
  2549. my $dec_pwd = '';
  2550. for my $char (map { pack('C', hex($_)) } ($token =~ /(..)/g)) {
  2551. my $decode=chop($key);
  2552. $dec_pwd.=chr(ord($char)^ord($decode));
  2553. $key=$decode.$key;
  2554. }
  2555. return $dec_pwd;
  2556. }
  2557. else {
  2558. Log3 $hash, 1, "TelegramBot_readToken: Error: No API token in file";
  2559. return "";
  2560. }
  2561. } # end readToken
  2562. #####################################
  2563. # INTERNAL: get only numeric part of a value (simple)
  2564. sub TelegramBot_AttrNum($$$)
  2565. {
  2566. my ($d,$n,$default) = @_;
  2567. my $val = AttrVal($d,$n,$default);
  2568. $val =~ s/[^-\.\d]//g;
  2569. return $val;
  2570. }
  2571. ######################################
  2572. # Get a string and identify possible media streams
  2573. # PNG is tested
  2574. # returns
  2575. # -1 for image
  2576. # -2 for Audio
  2577. # -3 for other media
  2578. # and extension without dot as 2nd list element
  2579. sub TelegramBot_IdentifyStream($$) {
  2580. my ($hash, $msg) = @_;
  2581. # signatures for media files are documented here --> https://en.wikipedia.org/wiki/List_of_file_signatures
  2582. # seems sometimes more correct: https://wangrui.wordpress.com/2007/06/19/file-signatures-table/
  2583. return (-1,"png") if ( $msg =~ /^\x89PNG\r\n\x1a\n/ ); # PNG
  2584. return (-1,"jpg") if ( $msg =~ /^\xFF\xD8\xFF/ ); # JPG not necessarily complete, but should be fine here
  2585. return (-2 ,"mp3") if ( $msg =~ /^\xFF\xF3/ ); # MP3 MPEG-1 Layer 3 file without an ID3 tag or with an ID3v1 tag
  2586. return (-2 ,"mp3") if ( $msg =~ /^\xFF\xFB/ ); # MP3 MPEG-1 Layer 3 file without an ID3 tag or with an ID3v1 tag
  2587. # MP3 MPEG-1 Layer 3 file with an ID3v2 tag
  2588. # starts with ID3 then version (most popular 03, new 04 seldom used, old 01 and 02) ==> Only 2,3 and 4 are tested currently
  2589. return (-2 ,"mp3") if ( $msg =~ /^ID3\x03/ );
  2590. return (-2 ,"mp3") if ( $msg =~ /^ID3\x04/ );
  2591. return (-2 ,"mp3") if ( $msg =~ /^ID3\x02/ );
  2592. return (-3,"pdf") if ( $msg =~ /^%PDF/ ); # PDF document
  2593. return (-3,"docx") if ( $msg =~ /^PK\x03\x04/ ); # Office new
  2594. return (-3,"docx") if ( $msg =~ /^PK\x05\x06/ ); # Office new
  2595. return (-3,"docx") if ( $msg =~ /^PK\x07\x08/ ); # Office new
  2596. return (-3,"doc") if ( $msg =~ /^\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1/ ); # Office old - D0 CF 11 E0 A1 B1 1A E1
  2597. return (0,undef);
  2598. }
  2599. #####################################
  2600. #####################################
  2601. # INTERNAL: prepare msg/ret for log file
  2602. sub TelegramBot_MsgForLog($;$) {
  2603. my ($msg, $stream) = @_;
  2604. if ( ! defined( $msg ) ) {
  2605. return "<undef>";
  2606. } elsif ( $stream ) {
  2607. return "<stream:".length($msg).">";
  2608. }
  2609. return $msg;
  2610. }
  2611. ######################################
  2612. # read binary file for Phototransfer - returns undef or empty string on error
  2613. #
  2614. sub TelegramBot_BinaryFileRead($$) {
  2615. my ($hash, $fileName) = @_;
  2616. return '' if ( ! (-e $fileName) );
  2617. my $fileData = '';
  2618. open TGB_BINFILE, '<'.$fileName;
  2619. binmode TGB_BINFILE;
  2620. while (my $line = <TGB_BINFILE>){
  2621. $fileData .= $line;
  2622. }
  2623. close TGB_BINFILE;
  2624. return $fileData;
  2625. }
  2626. ######################################
  2627. # write binary file for (hest hash, filename and the data
  2628. #
  2629. sub TelegramBot_BinaryFileWrite($$$) {
  2630. my ($hash, $fileName, $data) = @_;
  2631. open TGB_BINFILE, '>'.$fileName;
  2632. binmode TGB_BINFILE;
  2633. print TGB_BINFILE $data;
  2634. close TGB_BINFILE;
  2635. return undef;
  2636. }
  2637. ##############################################################################
  2638. ##############################################################################
  2639. ##
  2640. ## Documentation
  2641. ##
  2642. ##############################################################################
  2643. ##############################################################################
  2644. 1;
  2645. =pod
  2646. =item summary send and receive of messages through telegram instant messaging
  2647. =item summary_DE senden und empfangen von Nachrichten durch telegram IM
  2648. =begin html
  2649. <a name="TelegramBot"></a>
  2650. <h3>TelegramBot</h3>
  2651. <ul>
  2652. The TelegramBot module allows the usage of the instant messaging service <a href="https://telegram.org/">Telegram</a> from FHEM in both directions (sending and receiving).
  2653. So FHEM can use telegram for notifications of states or alerts, general informations and actions can be triggered.
  2654. <br>
  2655. <br>
  2656. TelegramBot makes use of the <a href=https://core.telegram.org/bots/api>telegram bot api</a> and does NOT rely on any addition local client installed.
  2657. <br>
  2658. Telegram Bots are different from normal telegram accounts, without being connected to a phone number. Instead bots need to be registered through the
  2659. <a href=https://core.telegram.org/bots#botfather>BotFather</a> to gain the needed token for authorizing as bot with telegram.org. This is done by connecting (in a telegram client) to the BotFather and sending the command <code>/newbot</code> and follow the steps specified by the BotFather. This results in a token, this token (e.g. something like <code>110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw</code> is required for defining a working telegram bot in fhem.
  2660. <br><br>
  2661. Bots also differ in other aspects from normal telegram accounts. Here some examples:
  2662. <ul>
  2663. <li>Bots can not initiate connections to arbitrary users, instead users need to first initiate the communication with the bot.</li>
  2664. <li>Bots have a different privacy setting then normal users (see <a href=https://core.telegram.org/bots#privacy-mode>Privacy mode</a>) </li>
  2665. <li>Bots support commands and specialized keyboards for the interaction </li>
  2666. </ul>
  2667. <br><br>
  2668. Note:
  2669. <ul>
  2670. <li>This module requires the perl JSON module.<br>
  2671. Please install the module (e.g. with <code>sudo apt-get install libjson-perl</code>) or the correct method for the underlying platform/system.</li>
  2672. <li>The attribute pollingTimeout needs to be set to a value greater than zero, to define the interval of receiving messages (if not set or set to 0, no messages will be received!)</li>
  2673. <li>Multiple infomations are stored in readings (esp contacts) and internals that are needed for the bot operation, so having an recent statefile will help in correct operation of the bot. Generally it is recommended to regularly store the statefile (see save command)</li>
  2674. </ul>
  2675. <br><br>
  2676. The TelegramBot module allows receiving of messages from any peer (telegram user) and can send messages to known users.
  2677. The contacts/peers, that are known to the bot are stored in a reading (named <code>Contacts</code>) and also internally in the module in a hashed list to allow the usage
  2678. of contact ids and also full names and usernames. Contact ids are made up from only digits, user names are prefixed with a @, group names are prefixed with a #.
  2679. All other names will be considered as full names of contacts. Here any spaces in the name need to be replaced by underscores (_).
  2680. Each contact is considered a triple of contact id, full name (spaces replaced by underscores) and username or groupname prefixed by @ respectively #.
  2681. The three parts are separated by a colon (:).
  2682. <br>
  2683. Contacts are collected automatically during communication by new users contacting the bot or users mentioned in messages.
  2684. <br><br>
  2685. Updates and messages are received via long poll of the GetUpdates message. This message currently supports a maximum of 20 sec long poll.
  2686. In case of failures delays are taken between new calls of GetUpdates. In this case there might be increasing delays between sending and receiving messages!
  2687. <br>
  2688. Beside pure text messages also media messages can be sent and received. This includes audio, video, images, documents, locations and venues.
  2689. <br><br>
  2690. <a name="TelegramBotdefine"></a>
  2691. <b>Define</b>
  2692. <ul>
  2693. <code>define &lt;name&gt; TelegramBot &lt;token&gt; </code>
  2694. <br><br>
  2695. Defines a TelegramBot device using the specified token perceived from botfather
  2696. <br>
  2697. Example:
  2698. <ul>
  2699. <code>define teleBot TelegramBot 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw</code><br>
  2700. </ul>
  2701. <br>
  2702. </ul>
  2703. <br><br>
  2704. <a name="TelegramBotset"></a>
  2705. <b>Set</b>
  2706. <ul>
  2707. <li><code>message|msg|_msg|send [ @&lt;peer1&gt; ... @&lt;peerN&gt; ] [ (&lt;keyrow1&gt;) ... (&lt;keyrowN&gt;) ] &lt;text&gt;</code><br>Sends the given message to the given peer or if peer(s) is ommitted currently defined default peer user. Each peer given needs to be always prefixed with a '@'. Peers can be specified as contact ids, full names (with underscore instead of space), usernames (prefixed with another @) or chat names (also known as groups in telegram groups must be prefixed with #). Multiple peers are to be separated by space<br>
  2708. A reply keyboard can be specified by adding a list of strings enclosed in parentheses "()". Each separate string will make one keyboard row in a reply keyboard. The different keys in the row need to be separated by |. The key strings can contain spaces.<br>
  2709. Messages do not need to be quoted if containing spaces. If you want to use parentheses at the start of the message than add one extra character before the parentheses (i.e. an underline) to avoid the message being parsed as a keyboard <br>
  2710. Examples:<br>
  2711. <dl>
  2712. <dt><code>set aTelegramBotDevice message @@someusername a message to be sent</code></dt>
  2713. <dd> to send to a peer having someusername as username (not first and last name) in telegram <br> </dd>
  2714. <dt><code>set aTelegramBotDevice message (yes) (may be) are you there?</code></dt>
  2715. <dd> to send the message "are you there?" and provide a reply keyboard with two buttons ("yes" and "may be") on separate rows to the default peer <br> </dd>
  2716. <dt><code>set aTelegramBotDevice message @@someusername (yes) (may be) are you there?</code></dt>
  2717. <dd> to send the message from above with reply keyboard to a peer having someusername as username <br> </dd>
  2718. <dt><code>set aTelegramBotDevice message (yes|no) (may be) are you there?</code></dt>
  2719. <dd> to send the message from above with reply keyboard having 3 keys, 2 in the first row ("yes" / "no") and a second row with just one key to the default peer <br> </dd>
  2720. <dt><code>set aTelegramBotDevice message @@someusername @1234567 a message to be sent to multiple receipients</code></dt>
  2721. <dd> to send to a peer having someusername as username (not first and last name) in telegram <br> </dd>
  2722. <dt><code>set aTelegramBotDevice message @Ralf_Mustermann another message</code></dt>
  2723. <dd> to send to a peer with Ralf as firstname and Mustermann as last name in telegram <br></dd>
  2724. <dt><code>set aTelegramBotDevice message @#justchatting Hello</code></dt>
  2725. <dd> to send the message "Hello" to a chat with the name "justchatting" <br></dd>
  2726. <dt><code>set aTelegramBotDevice message @1234567 Bye</code></dt>
  2727. <dd> to send the message "Bye" to a contact or chat with the id "1234567". Chat ids might be negative and need to be specified with a leading hyphen (-). <br></dd>
  2728. <dl>
  2729. </li>
  2730. <li><code>silentmsg, silentImage, silentInline ...</code><br>Sends the given message silently (with disabled_notifications) to the recipients. Syntax and parameters are the same as in the corresponding send/message command.
  2731. </li>
  2732. <li><code>msgForceReply [ @&lt;peer1&gt; ... @&lt;peerN&gt; ] &lt;text&gt;</code><br>Sends the given message to the recipient(s) and requests (forces) a reply. Handling of peers is equal to the message command. Adding reply keyboards is currently not supported by telegram.
  2733. </li>
  2734. <li><code>reply &lt;msgid&gt; [ @&lt;peer1&gt; ] &lt;text&gt;</code><br>Sends the given message as a reply to the msgid (number) given to the given peer or if peer is ommitted to the defined default peer user. Only a single peer can be specified. Beside the handling of the message as a reply to a message received earlier, the peer and message handling is otherwise identical to the msg command.
  2735. </li>
  2736. <li><code>msgEdit &lt;msgid&gt; [ @&lt;peer1&gt; ] &lt;text&gt;</code><br>Changes the given message on the recipients clients. The msgid of the message to be changed must match a valid msgId and the peers need to match the original recipient, so only a single peer can be given or if peer is ommitted the defined default peer user is used. Beside the handling of a change of an existing message, the peer and message handling is otherwise identical to the msg command.
  2737. </li>
  2738. <li><code>msgDelete &lt;msgid&gt; [ @&lt;peer1&gt; ] </code><br>Deletes the given message on the recipients clients. The msgid of the message to be changed must match a valid msgId and the peers need to match the original recipient, so only a single peer can be given or if peer is ommitted the defined default peer user is used. Restrictions apply for deleting messages in the Bot API as currently specified here (<a href=https://core.telegram.org/bots/api#deletemessage>deleteMessage</a>)
  2739. </li>
  2740. <li><code>favoritesMenu [ @&lt;peer&gt; ] </code><br>send the favorites menu to the corresponding peer if defined</code>
  2741. </li>
  2742. <li><code>cmdSend|cmdSendSilent [ @&lt;peer1&gt; ... @&lt;peerN&gt; ] &lt;fhem command&gt;</code><br>Executes the given fhem command and then sends the result to the given peers or the default peer (cmdSendSilent does the same as silent message).<br>
  2743. Example: The following command would sent the resulting SVG picture to the default peer: <br>
  2744. <code>set tbot cmdSend { plotAsPng('SVG_FileLog_Aussen') }</code>
  2745. </li>
  2746. <li><code>queryInline [ @&lt;peer1&gt; ... @&lt;peerN&gt; ] (&lt;keyrow1&gt;) ... (&lt;keyrowN&gt;) &lt;text&gt;</code><br>Sends the given message to the recipient(s) with an inline keyboard allowing direct response <br>
  2747. IMPORTANT: The response coming from the keyboard will be provided in readings and a corresponding answer command with the query id is required, sicne the client is frozen otherwise waiting for the response from the bot!
  2748. REMARK: inline queries are only accepted from contacts/peers that are authorized (i.e. as for executing commands, see cmdKeyword and cmdRestrictedPeer !)
  2749. </li>
  2750. <li><code>queryEditInline &lt;msgid&gt; [ @&lt;peer&gt; ] (&lt;keyrow1&gt;) ... (&lt;keyrowN&gt;) &lt;text&gt;</code><br>Updates the original message specified with msgId with the given message to the recipient(s) with an inline keyboard allowing direct response <br>
  2751. With this method interactive inline dialogs are possible, since the edit of message or inline keyboard can be done multiple times.
  2752. </li>
  2753. <li><code>queryAnswer &lt;queryid&gt; [ &lt;text&gt; ] </code><br>Sends the response to the inline query button press. The message is optional, the query id can be collected from the reading "callbackID". This call is mandatory on reception of an inline query from the inline command above
  2754. </li>
  2755. <li><code>sendImage|image [ @&lt;peer1&gt; ... @&lt;peerN&gt;] &lt;file&gt; [&lt;caption&gt;]</code><br>Sends a photo to the given peer(s) or if ommitted to the default peer.
  2756. File is specifying a filename and path to the image file to be send.
  2757. Local paths should be given local to the root directory of fhem (the directory of fhem.pl e.g. /opt/fhem).
  2758. Filenames with special characters (especially spaces) need to be given with url escaping (i.e. spaces need to be replaced by %20).
  2759. Rules for specifying peers are the same as for messages. Multiple peers are to be separated by space. Captions can also contain multiple words and do not need to be quoted.
  2760. </li>
  2761. <li><code>sendMedia|sendDocument [ @&lt;peer1&gt; ... @&lt;peerN&gt;] &lt;file&gt;</code><br>Sends a media file (video, audio, image or other file type) to the given peer(s) or if ommitted to the default peer. Handling for files and peers is as specified above.
  2762. </li>
  2763. <li><code>sendVoice [ @&lt;peer1&gt; ... @&lt;peerN&gt;] &lt;file&gt;</code><br>Sends a voice message for playing directly in the browser to the given peer(s) or if ommitted to the default peer. Handling for files and peers is as specified above.
  2764. </li>
  2765. <li><code>silentImage ...</code><br>Sends the given image silently (with disabled_notifications) to the recipients. Syntax and parameters are the same as in the sendImage command.
  2766. </li>
  2767. <br>
  2768. <li><code>sendLocation [ @&lt;peer1&gt; ... @&lt;peerN&gt;] &lt;latitude&gt; &lt;longitude&gt;</code><br>Sends a location as pair of coordinates latitude and longitude as floating point numbers
  2769. <br>Example: <code>set aTelegramBotDevice sendLocation @@someusername 51.163375 10.447683</code> will send the coordinates of the geographical center of Germany as location.
  2770. </li>
  2771. <br>
  2772. <li><code>replaceContacts &lt;text&gt;</code><br>Set the contacts newly from a string. Multiple contacts can be separated by a space.
  2773. Each contact needs to be specified as a triple of contact id, full name and user name as explained above. </li>
  2774. <li><code>reset</code><br>Reset the internal state of the telegram bot. This is normally not needed, but can be used to reset the used URL,
  2775. internal contact handling, queue of send items and polling <br>
  2776. ATTENTION: Messages that might be queued on the telegram server side (especially commands) might be then worked off afterwards immedately.
  2777. If in doubt it is recommened to temporarily deactivate (delete) the cmdKeyword attribute before resetting.</li>
  2778. <br>
  2779. <li><code>token &lt;apitoken&gt;</code><br>Specify a new APItoken to be stored for this bot
  2780. </li>
  2781. </ul>
  2782. <br><br>
  2783. <a name="TelegramBotget"></a>
  2784. <b>Get</b>
  2785. <ul>
  2786. <li><code>urlForFile &lt;fileid&gt;</code><br>Get a URL for a file id that was returned in a message
  2787. </li>
  2788. <li><code>update </code><br>Execute a single update (instead of automatic polling) - manual polling
  2789. </li>
  2790. <li><code>peerId &lt;peer&gt;</code><br>Ask for a peerId for a given peer, the peer can be specified in the same form as in a message without the initial '@'
  2791. </li>
  2792. </ul>
  2793. <br><br>
  2794. <a name="TelegramBotattr"></a>
  2795. <b>Attributes</b>
  2796. <br><br>
  2797. <ul>
  2798. <li><code>defaultPeer &lt;name&gt;</code><br>Specify contact id, user name or full name of the default peer to be used for sending messages. </li>
  2799. <li><code>defaultPeerCopy &lt;1 (default) or 0&gt;</code><br>Copy all command results also to the defined defaultPeer. If set results are sent both to the requestor and the defaultPeer if they are different.
  2800. </li>
  2801. <li><code>parseModeSend &lt;0_None or 1_Markdown or 2_HTML or 3_Inmsg &gt;</code><br>Specify the parse_mode (allowing formatting of text messages) for sent text messages. 0_None is the default where no formatting is used and plain text is sent. The different formatting options for markdown or HTML are described here <a href="https://core.telegram.org/bots/api/#formatting-options">https://core.telegram.org/bots/api/#formatting-options</a>. The option 3_Inmsg allows to specify the correct parse_mode at the beginning of the message (e.g. "Markdown*bold text*..." as message).
  2802. </li>
  2803. <li><code>webPagePreview &lt;1 or 0&gt;</code><br>Disable / Enable (Default = 1) web page preview on links in messages. See parameter https://core.telegram.org/bots/api/#sendmessage as described here: https://core.telegram.org/bots/api/#sendmessage
  2804. </li>
  2805. <br>
  2806. <li><code>cmdKeyword &lt;keyword&gt;</code><br>Specify a specific text that needs to be sent to make the rest of the message being executed as a command.
  2807. So if for example cmdKeyword is set to <code>ok fhem</code> then a message starting with this string will be executed as fhem command
  2808. (see also cmdTriggerOnly).<br>
  2809. NOTE: It is advised to set cmdRestrictedPeer for restricting access to this feature!<br>
  2810. Example: If this attribute is set to a value of <code>ok fhem</code> a message of <code>ok fhem attr telegram room IM</code>
  2811. send to the bot would execute the command <code>attr telegram room IM</code> and set a device called telegram into room IM.
  2812. The result of the cmd is sent to the requestor and in addition (if different) sent also as message to the defaultPeer (This can be controlled with the attribute <code>defaultPeerCopy</code>).
  2813. <br>
  2814. Note: <code>shutdown</code> is not supported as a command (also in favorites) and will be rejected. This is needed to avoid reexecution of the shutdown command directly after restart (endless loop !).
  2815. </li>
  2816. <li><code>cmdSentCommands &lt;keyword&gt;</code><br>Specify a specific text that will trigger sending the last commands back to the sender<br>
  2817. Example: If this attribute is set to a value of <code>last cmd</code> a message of <code>last cmd</code>
  2818. woud lead to a reply with the list of the last sent fhem commands will be sent back.<br>
  2819. Please also consider cmdRestrictedPeer for restricting access to this feature!<br>
  2820. </li>
  2821. <br>
  2822. <li><code>cmdFavorites &lt;keyword&gt;</code><br>Specify a specific text that will trigger sending the list of defined favorites or executes a given favorite by number (the favorites are defined in attribute <code>favorites</code>).
  2823. <br>
  2824. NOTE: It is advised to set cmdRestrictedPeer for restricting access to this feature!<br>
  2825. Example: If this attribute is set to a value of <code>favorite</code> a message of <code>favorite</code> to the bot will return a list of defined favorite commands and their index number. In the same case the message <code>favorite &lt;n&gt;</code> (with n being a number) would execute the command that is the n-th command in the favorites list. The result of the command will be returned as in other command executions.
  2826. </li>
  2827. <li><code>favorites &lt;list of commands&gt;</code><br>Specify a list of favorite commands for Fhem (without cmdKeyword). Multiple favorites are separated by a single semicolon (;). A double semicolon can be used to specify multiple commands for a single favorite <br>
  2828. <br>
  2829. Favorite commands are fhem commands with an optional alias for the command given. The alias can be sent as message (instead of the favoriteCmd) to execute the command. Before the favorite command also an alias (other shortcut for the favorite) or/and a descriptive text (description enclosed in []) can be specifed. If alias or description is specified this needs to be prefixed with a '/' and the alias if given needs to be specified first.
  2830. <br>
  2831. Favorites can also only be callable with the alias command and not via the corresponding favorite number and it will not be listed in the keyboard. For this the alias needs to be prefixed with a hyphen (-) after the leading slash
  2832. <br>
  2833. <br>
  2834. Example: Assuming cmdFavorites is set to a value of <code>favorite</code> and this attribute is set to a value of
  2835. <br><code>get lights status; /light=set lights on; /dark[Make it dark]=set lights off; /-heating=set heater; /[status]=get heater status;</code> <br>
  2836. <ul>
  2837. <li>Then a message "favorite1" to the bot would execute the command <code>get lights status</code></li>
  2838. <li>A message "favorite 2" or "/light" to the bot would execute the command <code>set lights on</code>. And the favorite would show as "make it dark" in the list of favorites.</li>
  2839. <li>A message "/heating on" to the bot would execute the command <code>set heater on</code><br> (Attention the remainder after the alias will be added to the command in fhem!). SInce this favorite is hidden only the alias can be used to call the favorite</li>
  2840. <li>A message "favorite 3" (since the one before is hidden) to the bot would execute the command <code>get heater status</code> and this favorite would show as "status" as a description in the favorite list</li>
  2841. </ul>
  2842. <br>
  2843. Favorite commands can also be prefixed with a question mark ('?') to enable a confirmation being requested before executing the command.
  2844. <br>
  2845. Examples: <code>get lights status; /light=?set lights on; /dark=set lights off; ?set heater;</code> <br>
  2846. <br>
  2847. <br>
  2848. Favorite commands can also be prefixed with a exclamation mark ('!') to ensure an ok-result message is sent even when the attribute cmdReturnEmptyResult is set to 0.
  2849. <br>
  2850. Examples: <code>get lights status; /light=!set lights on; /dark=set lights off; !set heater;</code> <br>
  2851. <br>
  2852. The question mark needs to be before the exclamation mark if both are given.
  2853. <br>
  2854. <br>
  2855. The description for an alias can also be prefixed with a '-'. In this case the favorite command/alias will not be shown in the favorite menu. This case only works for inline keyboard favorite menus.
  2856. <br>
  2857. <br>
  2858. Favorite commands can also include multiple fhem commands being execute using ;; as a separator
  2859. <br>
  2860. Example: <code>get lights status; /blink=set lights on;; sleep 3;; set lights off; set heater;</code> <br>
  2861. <br>
  2862. Meaning the full format for a single favorite is <code>/alias[description]=commands</code> where the alias can be empty if the description is given or <code>/alias=command</code> or <code>/-alias=command</code> for a hidden favorite or just the <code>commands</code>. In any case the commands can be also prefixed with a '?' or a '!' (or both). The description also can be given as <code>[-description]</code> to remvoe the command or alias from the favorite menus in inline keyboard menus. Spaces are only allowed in the description and the commands, usage of spaces in other areas might lead to wrong interpretation of the definition. Spaces and also many other characters are not supported in the alias commands by telegram, so if you want to have your favorite/alias directly recognized in the telegram app, restriction to letters, digits and underscore is required. Double semicolon will be used for specifying mutliple fhem commands in a single favorites, while single semicolon is used to separate between different favorite definitions
  2863. </li>
  2864. <li><code>favorites2Col &lt;1 or 0&gt;</code><br>Show favorites in 2 columns keyboard (instead of 1 column - default)
  2865. </li>
  2866. <li><code>favoritesInline &lt;1 or 0&gt;</code><br>When set to 1 it shows favorite dialog as inline keyboard and results will be also displayed inline (instead of as reply keyboards - default)
  2867. </li>
  2868. <br>
  2869. <li><code>cmdRestrictedPeer &lt;peer(s)&gt;</code><br>Restrict the execution of commands only to messages sent from the given peername or multiple peernames
  2870. (specified in the form of contact id, username or full name, multiple peers to be separated by a space). This applies to the internal machanisms for commands in the TelegramBot-Module (favorites, cmdKeyword etc) not for external methods to react on changes of readings.
  2871. A message with the cmd and sender is sent to the default peer in case of another peer trying to sent messages<br>
  2872. NOTE: It is recommended to use only peer ids for this restriction to reduce spoofing risk!
  2873. </li>
  2874. <li><code>cmdRespondChat &lt;1 or 0&gt;</code><br>Results / Responses from Commands will be sent to a group chat (1 = default) if originating from this chat. Otherwise responses will be sent only to the person initiating the command (personal chat) if set to value 0. <br>
  2875. Note: Group chats also need to be allowed as restricted Peer in cmdRestrictedPeer if this is set.
  2876. </li>
  2877. <li><code>allowUnknownContacts &lt;1 or 0&gt;</code><br>Allow new contacts to be added automatically (1 - Default) or restrict message reception only to known contacts and unknwown contacts will be ignored (0).
  2878. </li>
  2879. <li><code>saveStateOnContactChange &lt;1 or 0&gt;</code><br>Allow statefile being written on every new contact found, ensures new contacts not being lost on any loss of statefile. Default is on (1).
  2880. </li>
  2881. <li><code>cmdReturnEmptyResult &lt;1 or 0&gt;</code><br>Return empty (success) message for commands (default). Otherwise return messages are only sent if a result text or error message is the result of the command execution.
  2882. </li>
  2883. <li><code>allowedCommands &lt;list of command&gt;</code><br>Restrict the commands that can be executed through favorites and cmdKeyword to the listed commands (separated by space). Similar to the corresponding restriction in FHEMWEB. The allowedCommands will be set on the corresponding instance of an allowed device with the name "allowed_&lt;TelegrambotDeviceName&gt; and not on the telegramBotDevice! This allowed device is created and modified automatically.<br>
  2884. <b>ATTENTION: This is not a hardened secure blocking of command execution, there might be ways to break the restriction!</b>
  2885. </li>
  2886. <li><code>cmdTriggerOnly &lt;0 or 1&gt;</code><br>Restrict the execution of commands only to trigger command. If this attr is set (value 1), then only the name of the trigger even has to be given (i.e. without the preceding statement trigger).
  2887. So if for example cmdKeyword is set to <code>ok fhem</code> and cmdTriggerOnly is set, then a message of <code>ok fhem someMacro</code> would execute the fhem command <code>trigger someMacro</code>.<br>
  2888. Note: This is deprecated and will be removed in one of the next releases
  2889. </li>
  2890. <li><code>queryAnswerText &lt;text&gt;</code><br>Specify the automatic answering to buttons send through queryInline command. If this attribute is set an automatic answer is provided to the press of the inline button. The text in the attribute is evaluated through set-logic, so that readings and also perl code can be stored here. The result of the translation with set-logic will be sent as a text with the answer (this text is currently limited by telegram to 200 characters). <br>
  2891. Note: A value of "0" in the attribute or as result of the evaluation will result in no text being sent with the answer.
  2892. <br>
  2893. Note: If the peer sending the button is not authorized an answer is always sent without any text.
  2894. </li>
  2895. <br>
  2896. <li><code>pollingTimeout &lt;number&gt;</code><br>Used to specify the timeout for long polling of updates. A value of 0 is switching off any long poll.
  2897. In this case no updates are automatically received and therefore also no messages can be received. It is recommended to set the pollingtimeout to a reasonable time between 15 (not too short) and 60 (to avoid broken connections). See also attribute <code>disable</code>.
  2898. </li>
  2899. <li><code>pollingVerbose &lt;0_None 1_Digest 2_Log&gt;</code><br>Used to limit the amount of logging for errors of the polling connection. These errors are happening regularly and usually are not consider critical, since the polling restarts automatically and pauses in case of excess errors. With the default setting "1_Digest" once a day the number of errors on the last day is logged (log level 3). With "2_Log" every error is logged with log level 2. With the setting "0_None" no errors are logged. In any case the count of errors during the last day and the last error is stored in the readings <code>PollingErrCount</code> and <code>PollingLastError</code> </li>
  2900. <li><code>disable &lt;0 or 1&gt;</code><br>Used to disable the polling if set to 1 (default is 0).
  2901. </li>
  2902. <br>
  2903. <li><code>cmdTimeout &lt;number&gt;</code><br>Used to specify the timeout for sending commands. The default is a value of 30 seconds, which should be normally fine for most environments. In the case of slow or on-demand connections to the internet this parameter can be used to specify a longer time until a connection failure is considered.
  2904. </li>
  2905. <br>
  2906. <li><code>maxFileSize &lt;number of bytes&gt;</code><br>Maximum file size in bytes for transfer of files (images). If not set the internal limit is specified as 10MB (10485760B).
  2907. </li>
  2908. <li><code>filenameUrlEscape &lt;0 or 1&gt;</code><br>Specify if filenames can be specified using url escaping, so that special chanarcters as in URLs. This specifically allows to specify spaces in filenames as <code>%20</code>. Default is off (0).
  2909. </li>
  2910. <li><code>maxReturnSize &lt;number of chars&gt;</code><br>Maximum size of command result returned as a text message including header (Default is unlimited). The internal shown on the device is limited to 1000 chars.
  2911. </li>
  2912. <li><code>maxRetries &lt;0,1,2,3,4,5&gt;</code><br>Specify the number of retries for sending a message in case of a failure. The first retry is sent after 10sec, the second after 100, then after 1000s (~16min), then after 10000s (~2.5h), then after approximately a day. Setting the value to 0 (default) will result in no retries.
  2913. </li>
  2914. <br>
  2915. <li><code>textResponseConfirm &lt;TelegramBot FHEM : $peer\n Bestätigung \n&gt;</code><br>Text to be sent when a confirmation for a command is requested. Default is shown here and $peer will be replaced with the actual contact full name if added.
  2916. </li>
  2917. <li><code>textResponseFavorites &lt;TelegramBot FHEM : $peer\n Favoriten \n&gt;</code><br>Text to be sent as starter for the list of favorites. Default is shown here and $peer will be replaced with the actual contact full name if added.
  2918. </li>
  2919. <li><code>textResponseCommands &lt;TelegramBot FHEM : $peer\n Letzte Befehle \n&gt;</code><br>Text to be sent as starter for the list of last commands. Default is shown here and $peer will be replaced with the actual contact full name if added.
  2920. </li>
  2921. <li><code>textResponseResult &lt;TelegramBot FHEM : $peer\n Befehl:$cmd:\n Ergebnis:\n$result\n&gt;</code><br>Text to be sent as result for a cmd execution. Default is shown here and $peer will be replaced with the actual contact full name if added. Similarly $cmd and $result will be replaced with the cmd and the execution result. If the result is a response with just spaces, or other separator characters the result will be not sent at all (i.e. a values of "\n") will result in no message at all.
  2922. </li>
  2923. <li><code>textResponseUnauthorized &lt;UNAUTHORIZED: TelegramBot FHEM request from user :$peer\n Msg: $msg&gt;</code><br>Text to be sent as warning for unauthorized command requests. Default is shown here and $peer will be replaced with the actual contact full name and id if added. $msg will be replaced with the sent message.
  2924. </li>
  2925. <li><code>utf8Special &lt;0 or 1&gt;</code><br>Specify if utf8 encodings will be resolved before sending to avoid issues with timeout on HTTP send (experimental ! / default is off).
  2926. </li>
  2927. </ul>
  2928. <br><br>
  2929. <a name="TelegramBotreadings"></a>
  2930. <b>Readings</b>
  2931. <br><br>
  2932. <ul>
  2933. <li>Contacts &lt;text&gt;<br>The current list of contacts known to the telegram bot.
  2934. Each contact is specified as a triple in the same form as described above. Multiple contacts separated by a space. </li>
  2935. <br>
  2936. <li>msgId &lt;text&gt;<br>The id of the last received message is stored in this reading.
  2937. For secret chats a value of -1 will be given, since the msgIds of secret messages are not part of the consecutive numbering</li>
  2938. <li>msgPeer &lt;text&gt;<br>The sender name of the last received message (either full name or if not available @username)</li>
  2939. <li>msgPeerId &lt;text&gt;<br>The sender id of the last received message</li>
  2940. <li>msgChat &lt;text&gt;<br>The name of the Chat in which the last message was received (might be the peer if no group involved)</li>
  2941. <li>msgChatId &lt;ID&gt;<br>The id of the chat of the last message, if not identical to the private peer chat then this value will be the peer id</li>
  2942. <li>msgText &lt;text&gt;<br>The last received message text is stored in this reading. Information about special messages like documents, audio, video, locations or venues will be also stored in this reading</li>
  2943. <li>msgFileId &lt;fileid&gt;<br>The last received message file_id (Audio, Photo, Video, Voice or other Document) is stored in this reading.</li>
  2944. <li>msgReplyMsgId &lt;text&gt;<br>Contains the message id of the original message, that this message was a reply to</li>
  2945. <br>
  2946. <li>prevMsgId &lt;text&gt;<br>The id of the SECOND last received message is stored in this reading</li>
  2947. <li>prevMsgPeer &lt;text&gt;<br>The sender name of the SECOND last received message (either full name or if not available @username)</li>
  2948. <li>prevMsgPeerId &lt;text&gt;<br>The sender id of the SECOND last received message</li>
  2949. <li>prevMsgText &lt;text&gt;<br>The SECOND last received message text is stored in this reading</li>
  2950. <li>prevMsgFileId &lt;fileid&gt;<br>The SECOND last received file id is stored in this reading</li>
  2951. <br><b>Note: All prev... Readings are not triggering events</b><br>
  2952. <br>
  2953. <li>sentMsgId &lt;text&gt;<br>The id of the last sent message is stored in this reading, if not succesful the id is empty</li>
  2954. <li>sentMsgResult &lt;text&gt;<br>The result of the send process for the last message is contained in this reading - SUCCESS if succesful</li>
  2955. <br>
  2956. <li>StoredCommands &lt;text&gt;<br>A list of the last commands executed through TelegramBot. Maximum 10 commands are stored.</li>
  2957. <br>
  2958. <li>PollingErrCount &lt;number&gt;<br>Show the number of polling errors during the last day. The number is reset at the beginning of the next day.</li>
  2959. <li>PollingLastError &lt;number&gt;<br>Last error message that occured during a polling update call</li>
  2960. <br>
  2961. <li>callbackID &lt;id&gt; / callbackPeerId &lt;peer id&gt; / callbackPeer &lt;peer&gt;<br>Contains the query ID (respective the peer id and peer name) of the last received inline query from an inline query button (see set ... inline)</li>
  2962. </ul>
  2963. <br><br>
  2964. <a name="TelegramBotexamples"></a>
  2965. <b>Examples</b>
  2966. <br><br>
  2967. <ul>
  2968. <li>Send a telegram message if fhem has been newly started
  2969. <p>
  2970. <code>define notify_fhem_reload notify global:INITIALIZED set &lt;telegrambot&gt; message fhem started - just now </code>
  2971. </p>
  2972. </li>
  2973. <br>
  2974. <li>A command, that will retrieve an SVG plot and send this as a message back (can be also defined as a favorite).
  2975. <p>
  2976. Send the following message as a command to the bot <code>ok fhem { plotAsPng('SVG_FileLog_Aussen') }</code>
  2977. <br>assuming <code>ok fhem</code> is the command keyword)
  2978. </p> (
  2979. The png picture created by plotAsPng will then be send back in image format to the telegram client. This also works with other pictures returned and should also work with other media files (e.g. MP3 and doc files). The command can also be defined in a favorite.<br>
  2980. Remark: Example requires librsvg installed
  2981. </li>
  2982. <br>
  2983. <li>Allow telegram bot commands to be used<br>
  2984. If the keywords for commands are starting with a slash (/), the corresponding commands can be also defined with the
  2985. <a href=http://botsfortelegram.com/project/the-bot-father/>Bot Father</a>. So if a slash is typed a list of the commands will be automatically shown. Assuming that <code>cmdSentCommands</code> is set to <code>/History</code>. Then you can initiate the communication with the botfather, select the right bot and then with the command <code>/setcommands</code> define one or more commands like
  2986. <p>
  2987. <code>History-Show a history of the last 10 executed commands</code>
  2988. </p>
  2989. When typing a slash, then the text above will immediately show up in the client.
  2990. </li>
  2991. </ul>
  2992. </ul>
  2993. =end html
  2994. =cut