BastliBridge

git clone git://xatko.vsos.ethz.ch/BastliBridge.git
Log | Files | Refs | Submodules | README

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