telegram.d (7877B)
1 module telegram.telegram; 2 3 import telegram.http; 4 import std.stdio; 5 import std.socket; 6 import std.range; 7 import std.algorithm; 8 import std.format; 9 import std.conv; 10 import std.experimental.logger; 11 import std.traits; 12 import ssl.socket; 13 import std.file; 14 import std.typecons; 15 import std.string; 16 import std.json; 17 import std.exception; 18 import std.datetime :SysTime,Clock; 19 20 21 void wait(Socket sock) @trusted{ 22 version(Windows){ 23 static assert(false); 24 } 25 import core.sys.posix.sys.socket; 26 ubyte[4] buf; 27 recv(sock.handle, buf.ptr, buf.length, MSG_PEEK); 28 } 29 30 class TelegramErrnoException : ErrnoException{ 31 this(string msg, string file=__FILE__, size_t line=__LINE__){ 32 super(msg,file,line); 33 } 34 } 35 class TelegramException : Exception{ 36 this(string msg, string file=__FILE__, size_t line=__LINE__, Throwable next=null){ 37 super(msg,file,line,next); 38 } 39 } 40 41 class TelegramError : Error{ 42 this(string msg, string file=__FILE__, size_t line=__LINE__, Throwable next=null){ 43 super(msg,file,line,next); 44 } 45 } 46 47 ubyte[] receiveDynamic(Socket sock, ubyte[] buffer, size_t chunk_size=1024){ 48 ubyte[] buf=buffer; 49 size_t pos=0,res=0; 50 while(true){ 51 trace("Reading on ", sock.handle, "(", sock , ") ", " with buffer ", buf.ptr, " of length ", buf.length); 52 res=sock.receive(buf); 53 if(res==0){ 54 info("Socket ", sock.handle, " died"); 55 return []; 56 } 57 else if(res==Socket.ERROR){ 58 throw new ErrnoException("Error reading from socket"); 59 } 60 pos+=res; 61 if(pos<buffer.length) 62 break; 63 if(pos>=buffer.length){ 64 buffer.length+=chunk_size; 65 } 66 buf=buffer[pos..$]; 67 } 68 return buffer[0..pos]; 69 } 70 71 struct Telegram{ 72 string token=""; 73 enum ApiAddr="api.telegram.org"; 74 enum ApiPort=443; 75 76 long lastUpdate=-1; 77 78 SslSocket sock; 79 80 private Address ApiAddrObj; 81 82 private HttpRequest!void httpRequest; 83 private static immutable httpBufferChunk=1024; 84 private ubyte[] httpBuffer=new ubyte[httpBufferChunk]; 85 86 @disable this(); 87 this(string token){ 88 this.token=token; 89 } 90 91 void connect(bool test=true){ 92 if(!ApiAddrObj){ 93 getApiAddress(); 94 } 95 trace("Opening socket to Telegram on "~ApiAddrObj.to!string); 96 auto af=ApiAddrObj.addressFamily; 97 sock=new SslSocket(af); 98 sock.connect(ApiAddrObj); 99 scope(failure){ 100 sock.close(); 101 } 102 if(test){ 103 //Try whether the socket works 104 trace("Trying to get the Botname via newly opened socket "~sock.handle.to!string); 105 botName=getBotName(); 106 } 107 } 108 109 auto wait(){ 110 return sock.wait(); 111 } 112 113 void getApiAddress(){ 114 ApiAddrObj=getAddress(ApiAddr,ApiPort).front; 115 } 116 117 118 void disconnect(){ 119 sock.shutdown(SocketShutdown.BOTH); 120 sock.close(); 121 sock.destroy(); 122 } 123 124 auto getURI(in char[] method){ 125 return chain("bot",token,"/",method); 126 } 127 128 void httpRequestHeaders(ref HttpRequest!void httpRequest){ 129 httpRequest.header("Host", ApiAddr); 130 httpRequest.header("Connection", "keep-alive"); 131 } 132 133 void http(in char[] method){ 134 httpRequest.request("GET", getURI(method)); 135 httpRequestHeaders(httpRequest); 136 httpRequest.perform(sock); 137 } 138 139 void post(T)(in char[] method, T data){ 140 httpRequest.request("POST", getURI(method)); 141 httpRequestHeaders(httpRequest); 142 httpRequest.header("Content-Type", "application/x-www-form-urlencoded"); 143 httpRequest.data(data); 144 httpRequest.perform(sock); 145 } 146 147 void json(in char[] method, in JSONValue jv){ 148 httpRequest.request("POST", getURI(method)); 149 httpRequestHeaders(httpRequest); 150 httpRequest.header("Content-Type", "application/json"); 151 httpRequest.data(jv.toString); 152 httpRequest.perform(sock); 153 } 154 155 /** 156 * Receives all available data from the socket. 157 * The returned buffer gets reused, if you want it immutable, 158 * use .idup on it. 159 */ 160 const(char)[] receive() 161 in 162 { 163 assert(sock !is null); 164 assert(sock.isAlive); 165 } 166 body{ 167 static immutable size_t chunk_size=1024; 168 169 static ubyte[] buffer=new ubyte[chunk_size]; //This initialization is static, i.e. only done once 170 return cast(char[])sock.receiveDynamic(buffer, chunk_size); 171 } 172 173 void checkCode(T)(in HttpResponse!T res){ 174 if(res.code!=200){ 175 throw new TelegramException(format!"Server returned %d\n%s"(res.code, res.str)); 176 } 177 } 178 179 auto getJSON(T)(in HttpResponse!T res){ 180 auto j=parseJSON(res.str); 181 if(j["ok"].type != JSON_TYPE.TRUE){ 182 throw new TelegramException("API request failed: "~j["description"].str); 183 } 184 return j["result"]; 185 } 186 187 Nullable!(JSONValue) response(){ 188 auto h=receiveHttpResponse(sock, httpBuffer, httpBufferChunk); 189 trace("Got response from Telegram: ", h.to!string); 190 if(h.isNull){ 191 //throw new TelegramError("Socket closed"); 192 warning("Telegram closed _another_ socket :/ ",sock.handle); 193 reconnectSocket(); 194 return Nullable!JSONValue.init; 195 } 196 checkCode(h); 197 return Nullable!JSONValue(getJSON(h)); 198 } 199 200 void reconnectSocket(){ 201 trace("Reconnecting socket ", sock.handle); 202 disconnect(); 203 connect(); 204 trace("Reconnected socket, is now ", sock.handle); 205 } 206 207 struct HTTP{ 208 string ver; 209 ushort code; 210 string code_name; 211 const(char)[] content; 212 213 static HTTP parse(const(char)[] buf) 214 in{ 215 assert(buf.length>0); 216 } 217 body{ 218 HTTP res; 219 trace("Parseing "~buf); 220 auto lines=buf.splitter("\r\n"); 221 auto status=lines.front; 222 auto split=status.splitter(" "); 223 224 res.ver=split.front.idup; 225 split.popFront(); 226 res.code=split.front.to!ushort; 227 split.popFront(); 228 res.code_name=split.joiner.to!string; 229 230 res.content=buf.findSplit("\r\n\r\n")[2]; 231 trace("Parsed to "~res.to!string); 232 return res; 233 } 234 235 void checkCode(){ 236 if(code!=200){ 237 throw new TelegramException(format!"Server returned %d\n%s"(code, content)); 238 } 239 } 240 241 auto getJSON(){ 242 import std.json; 243 244 auto j=parseJSON(content); 245 if(j["ok"].type != JSON_TYPE.TRUE){ 246 throw new TelegramException("API request failed: "~j["description"].str); 247 } 248 return j["result"]; 249 } 250 } 251 252 bool updateRunning=false; 253 254 void triggerUpdates(uint timeout=600){ 255 if(updateRunning){ 256 warning("Update called in a row without a reply"); 257 return; 258 } 259 updateRunning=true; 260 trace("Triggering Update with timeout ", timeout, " lastupdate ",lastUpdate); 261 string params=format!"offset=%d&timeout=%d"(lastUpdate+1,timeout); 262 post("getUpdates",params); 263 } 264 265 void delegate(JSONValue jv)[] onMessage; 266 267 bool read(){ 268 updateRunning=false; 269 auto updates=response(); 270 if(updates.isNull){ 271 triggerUpdates(); 272 return true; 273 } 274 foreach(size_t i, update; updates){ 275 lastUpdate=max(lastUpdate, update["update_id"].integer); 276 if("message" in update){ 277 onMessage.each!(a=>a(update["message"])); 278 } 279 } 280 281 return false; 282 } 283 284 void send(in long chatid, in char[] buf){ 285 trace("Sending "~buf~" to "~chatid.to!string); 286 JSONValue j; 287 j.object=null; 288 j["chat_id"] = JSONValue(chatid); 289 j["text"] = JSONValue(buf); 290 trace("Sending telegram message "~j.toPrettyString); 291 json("sendMessage", j); 292 auto res=response(); 293 if(res.isNull){ 294 send(chatid,buf); 295 } 296 } 297 298 JSONValue getChat(T)(in T chatid) if(isNarrowString!T || is(T==long)){ 299 JSONValue j; 300 j.object=null; 301 j["chat_id"]=JSONValue(chatid); 302 json("getChat", j); 303 auto res=response(); 304 if(res.isNull){ 305 return getChat(chatid); 306 } 307 return res.get; 308 } 309 long getChatId(in char[] chatid){ 310 return getChat(chatid)["id"].integer; 311 } 312 string getChatName(long chatid){ 313 auto j=getChat(chatid); 314 if("title" in j){ 315 //We are in a group 316 return j["title"].str; 317 } 318 //We are in a query 319 return j["username"].str; 320 } 321 322 string botName; 323 324 string getBotName(){ 325 http("getMe"); 326 string _botName=response()["username"].str; 327 return _botName; 328 } 329 330 JSONValue getFile(in char[] file_id){ 331 JSONValue j; 332 j.object=null; 333 j["file_id"]=file_id; 334 json("getFile", j); 335 auto r=response(); 336 if(r.isNull){ 337 return getFile(file_id); 338 } 339 j=r.get; 340 auto v="file_path" in j; 341 if(v) 342 return j; 343 else 344 throw new TelegramException(cast(string)("Path of file "~file_id~" was not found")); 345 } 346 } 347