commit 0470aa8af0d96b1960cddad2c6830d97b2bd4464
Author: Dominik Schmidt <das1993@hotmail.com>
Date: Wed, 13 Sep 2017 17:37:55 +0200
Initial commit
Diffstat:
5 files changed, 704 insertions(+), 0 deletions(-)
diff --git a/.gitmodules b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "Dirk"]
+ path = Dirk
+ url = https://github.com/JakobOvrum/Dirk.git
diff --git a/Dirk b/Dirk
@@ -0,0 +1 @@
+Subproject commit e09e8b4456ab186cf0d425e233f2d37e2669a59c
diff --git a/Makefile b/Makefile
@@ -0,0 +1,29 @@
+DMD ?= ldmd2
+DEBUG ?= -g
+RELEASE ?= -release -O
+DFLAGS ?=
+INCLUDES = Dirk/source/
+SRC := src/telegram.d $(wildcard Dirk/source/*/*.d) Dirk/libev/deimos/ev.d
+TARGET = bastlibridge
+LIBS = ev
+
+_DFLAGS := $(addprefix -I, $(INCLUDES)) $(addprefix -L-l, $(LIBS)) $(DFLAGS)
+
+.PHONY: all shared static clean distclean
+
+all: $(TARGET)-debug
+
+$(TARGET): $(patsubst %.d, %.o, $(SRC))
+ $(DMD) $^ $(_DFLAGS) $(RELEASE) -of$@
+$(TARGET)-debug: $(patsubst %.d, %-debug.o, $(SRC))
+ $(DMD) $^ $(_DFLAGS) $(DEBUG) -of$@
+
+%.o:%.d
+ $(DMD) $(_DFLAGS) $(RELEASE) -c -of$@ $<
+%-debug.o:%.d
+ $(DMD) $(_DFLAGS) $(DEBUG) -c -of$@ $<
+
+clean:
+ rm -f $(patsubst %.d, %.o, $(SRC)) $(patsubst %.d,%-debug.o,$(SRC))
+distclean: clean
+ rm -f $(TARGET){,-debug}
diff --git a/README.md b/README.md
@@ -0,0 +1,20 @@
+Bastli IRC Bridge
+=================
+
+This Bot connects to the Telegram Bot API, as well as an IRC server, and bridges between them.
+
+Building
+=======
+
+ git submodule update --init
+ make
+
+Running
+=======
+
+Depending on whether you built the debug-binary, you run
+
+ ./bastlibridge-debug <Telegram API-Key>
+or
+
+ ./bastlibridge <Telegram API-Key>
diff --git a/src/telegram.d b/src/telegram.d
@@ -0,0 +1,651 @@
+import std.stdio;
+import std.socket;
+import std.range;
+import std.algorithm;
+import std.format;
+import std.conv;
+import std.experimental.logger;
+import std.traits;
+import ssl.socket;
+import irc.client;
+import std.file;
+import std.typecons;
+import std.string;
+import std.json;
+import std.exception;
+import deimos.ev;
+
+class TelegramErrnoException : ErrnoException{
+ this(string msg, string file=__FILE__, size_t line=__LINE__){
+ super(msg,file,line);
+ }
+}
+class TelegramException : Exception{
+ this(string msg, string file=__FILE__, size_t line=__LINE__, Throwable next=null){
+ super(msg,file,line,next);
+ }
+}
+
+class TelegramError : Error{
+ this(string msg, string file=__FILE__, size_t line=__LINE__, Throwable next=null){
+ super(msg,file,line,next);
+ }
+}
+
+
+struct Telegram{
+ string token="";
+ enum ApiAddr="api.telegram.org";
+ enum ApiPort=443;
+
+ long lastUpdate=-1;
+
+ package SslSocket sock;
+ package SslSocket sock2;
+
+ private Address ApiAddrObj;
+
+ @disable this();
+ this(string token){
+ this.token=token;
+ }
+
+ void connect(){
+ openSocket(sock);
+ openSocket(sock2);
+ }
+
+ void getApiAddress(){
+ ApiAddrObj=getAddress(ApiAddr,ApiPort).front;
+ }
+
+ void openSocket(T)(out T sock){
+ if(!ApiAddrObj){
+ getApiAddress();
+ }
+ trace("Opening socket to Telegram on "~ApiAddrObj.to!string);
+ auto af=ApiAddrObj.addressFamily;
+ sock=new T(af);
+ sock.connect(ApiAddrObj);
+ scope(failure){
+ sock.close();
+ }
+ testSocket(sock);
+ }
+
+ void testSocket(T)(T sock){
+ //Try whether the socket works
+ trace("Trying to get the Botname via newly opened socket "~sock.handle.to!string);
+ botName=getBotName(sock);
+ }
+
+
+ void disconnect(){
+ sock.shutdown(SocketShutdown.BOTH);
+ sock.close();
+ delete sock;
+ sock2.shutdown(SocketShutdown.BOTH);
+ sock2.close();
+ delete sock2;
+ }
+
+ auto getURI(in char[] method){
+ return chain("/bot",token,"/",method);
+ }
+
+ void http(in char[] method){
+ http(method, sock);
+ }
+ void http(in char[] method, Socket sock)
+ in{
+ assert(sock !is null);
+ assert(sock.isAlive);
+ }
+ body{
+ import std.conv:to;
+ string req="GET "~getURI(method).to!string~" HTTP/1.1\r\n"
+ ~"Host: "~ApiAddr~"\r\n"
+ ~"Connection: keep-alive\r\n";
+ req~="\r\n\r\n";
+ sock.send(req);
+ }
+
+ void data(in char[] method, in char[] data, in char[] datatype){
+ return this.data(method,data,datatype,sock);
+ }
+
+ void data(in char[] method, in char[] data, in char[] datatype, Socket sock)
+ in{
+ assert(sock !is null);
+ assert(sock.isAlive);
+ }
+ body{
+ import std.conv:to;
+ string req="GET "~getURI(method).to!string~" HTTP/1.1\r\n"
+ ~"Host: "~ApiAddr~"\r\n"
+ ~"Connection: keep-alive\r\n";
+ req~="Content-Length: "~data.length.to!string~"\r\n"
+ ~"Content-Type: "~datatype;
+ req~="\r\n\r\n"~data;
+ trace(req);
+ sock.send(req);
+ }
+
+ void post(in char[] method, in char[] data){
+ post(method,data,sock);
+ }
+ void post(in char[] method, in char[] data, Socket sock){
+ return this.data(method, data, "application/x-www-form-urlencoded",sock);
+ }
+ void json(in char[] method, in char[] data){
+ json(method,data,sock);
+ }
+ void json(in char[] method, in char[] data, Socket sock){
+ return this.data(method, data, "application/json",sock);
+ }
+
+ /**
+ * Receives all available data from the socket.
+ * The returned buffer gets reused, if you want it immutable,
+ * use .idup on it.
+ */
+ const(char)[] receive(){
+ return receive(sock);
+ }
+ const(char)[] receive(Socket sock)
+ in
+ {
+ assert(sock !is null);
+ assert(sock.isAlive);
+ }
+ body{
+ static immutable size_t chunk_size=1024;
+ static ubyte[] buffer=new ubyte[chunk_size]; //This initialization is static, i.e. only done once
+ ubyte[] buf=buffer;
+ size_t pos=0,res=0;
+ while(true){
+ res=sock.receive(buf);
+ if(res==0){
+ info("Socket ", sock.handle, " died");
+ sock.close();
+ return [];
+ }
+ else if(res==Socket.ERROR){
+ throw new TelegramErrnoException("Error reading from socket");
+ }
+ pos+=res;
+ if(pos<buffer.length)
+ break;
+ if(pos>=buffer.length){
+ buffer.length+=chunk_size;
+ }
+ buf=buffer[pos..$];
+ }
+ return cast(const(char)[])buffer[0..pos];
+ }
+
+ Nullable!(JSONValue) response(Socket sock){
+ auto buf=receive(sock);
+ if(buf.length==0){
+ //throw new TelegramError("Socket closed");
+ warning("Telegram closed _another_ socket :/ ",sock.handle);
+ reconnectSocket(sock);
+ return Nullable!JSONValue.init;
+ }
+ HTTP h=HTTP.parse(buf);
+ h.checkCode();
+ return Nullable!JSONValue(h.getJSON());
+ }
+
+ void reconnectSocket(Socket sock){
+ sock.shutdown(SocketShutdown.BOTH);
+ sock.close();
+ //dreckige hackerei
+ void[] mem=(cast(void*)sock)[0..__traits(classInstanceSize, SslSocket)];
+ emplace!SslSocket(mem,ApiAddrObj.addressFamily);
+ sock.connect(ApiAddrObj);
+ }
+
+ struct HTTP{
+ string ver;
+ ushort code;
+ string code_name;
+ const(char)[] content;
+
+ static HTTP parse(const(char)[] buf)
+ in{
+ assert(buf.length>0);
+ }
+ body{
+ HTTP res;
+ trace("Parseing "~buf);
+ auto lines=buf.splitter("\r\n");
+ auto status=lines.front;
+ auto split=status.splitter(" ");
+
+ res.ver=split.front.idup;
+ split.popFront();
+ res.code=split.front.to!ushort;
+ split.popFront();
+ res.code_name=split.joiner.to!string;
+
+ res.content=buf.findSplit("\r\n\r\n")[2];
+ trace("Parsed to "~res.to!string);
+ return res;
+ }
+
+ void checkCode(){
+ if(code!=200){
+ throw new TelegramException(format!"Server returned %d\n%s"(code, content));
+ }
+ }
+
+ auto getJSON(){
+ import std.json;
+
+ auto j=parseJSON(content);
+ if(j["ok"].type != JSON_TYPE.TRUE){
+ throw new TelegramException("API request failed: "~j["description"].str);
+ }
+ return j["result"];
+ }
+ }
+
+ void triggerUpdates(uint timeout=600){
+ string params=format!"offset=%d&timeout=%d"(lastUpdate+1,timeout);
+ post("getUpdates",params);
+ }
+
+ void delegate(JSONValue jv)[] onMessage;
+
+ bool read(){
+ auto buf=cast(const(char)[])receive();
+ if(buf.length==0){
+ return true;
+ }
+
+ HTTP response=HTTP.parse(buf);
+ response.checkCode();
+
+ auto updates=response.getJSON();
+ foreach(size_t i, update; updates){
+ lastUpdate=max(lastUpdate, update["update_id"].integer);
+ writeln(lastUpdate);
+ if("message" in update){
+ onMessage.each!(a=>a(update["message"]));
+ }
+ }
+
+ return false;
+ }
+
+ void send(in long chatid, in char[] buf){
+ trace("Sending "~buf~" to "~chatid.to!string);
+ JSONValue j;
+ j.object=null;
+ j["chat_id"] = JSONValue(chatid);
+ j["text"] = JSONValue(buf);
+ trace("Sending telegram message "~j.toPrettyString);
+ json("sendMessage", j.toString,sock2);
+ auto res=response(sock2);
+ if(res.isNull){
+ send(chatid,buf);
+ }
+ }
+
+ JSONValue getChat(T)(in T chatid) if(isNarrowString!T || is(T==long)){
+ JSONValue j;
+ j.object=null;
+ j["chat_id"]=JSONValue(chatid);
+ string data=j.toString;
+ json("getChat", data, sock2);
+ auto res=response(sock2);
+ if(res.isNull){
+ return getChat(chatid);
+ }
+ return res.get;
+ }
+ long getChatId(in char[] chatid){
+ return getChat(chatid)["id"].integer;
+ }
+ string getChatName(long chatid){
+ auto j=getChat(chatid);
+ if("title" in j){
+ //We are in a group
+ return j["title"].str;
+ }
+ //We are in a query
+ return j["username"].str;
+ }
+
+ string botName;
+
+ string getBotName(){
+ return getBotName(sock);
+ }
+ string getBotName(Socket sock){
+ http("getMe", sock);
+ string _botName=response(sock)["username"].str;
+ return _botName;
+ }
+}
+
+struct LookupTable{
+ private long[][string] _irc;
+ private string[][long] _telegram;
+ void connect(string irc, long telegram){
+ _irc[irc]~=telegram;
+ _telegram[telegram]~=irc;
+ }
+ void disconnect(string irc, long telegram){
+ auto i=irc in _irc;
+ *i=remove!(a=>a==telegram)(*i);
+ auto t=telegram in _telegram;
+ *t=remove!(a=>a==irc)(*t);
+ }
+
+ long[] irc(in char[] irc){
+ return _irc.get(cast(string)irc,[]);
+ }
+
+ string[] telegram(in long telegram){
+ return _telegram.get(telegram,[]);
+ }
+ auto ircChannels(){
+ return _irc.byKey;
+ }
+ auto telegramChannels(){
+ return _telegram.byKey;
+ }
+
+ auto links(){
+ return _irc.byPair.map!(a=>a[1].map!(b=>tuple(a[0],b))).joiner;
+ }
+}
+
+
+struct Watcher{
+ ev_io io;
+ void delegate(int revents) cb;
+ extern(C) static void callback(ev_loop_t* loop, ev_io *io, int revents){
+ auto watcher=cast(Watcher*) io;
+ watcher.cb(revents);
+ }
+ this(Socket sock, typeof(this.cb) cb){
+ this(sock.handle,cb);
+ }
+ this(int fd, typeof(this.cb) cb){
+ this.cb=cb;
+ ev_io_init(&io, &callback, fd, EV_READ);
+ }
+}
+
+struct Bot{
+ Address ircAddress;
+ Socket ircSocket;
+ IrcClient ircClient;
+ Telegram telegram;
+ LookupTable lut;
+ Watcher w_irc,w_tele,w_stdin;
+ ev_loop_t *eventloop;
+
+ string[long] telegram_channels;
+
+ this(string tgramApiKey){
+ telegram=Telegram("253031348:AAGn4dHCPwFDSN4Bmt4ZWc3bIAmARmzsf9k");
+ }
+
+ void initialize(){
+ ircAddress=getAddress("irc.freenode.net",6697)[0];
+ auto af=ircAddress.addressFamily;
+ ircSocket=new SslSocket(af);
+ ircClient=new IrcClient(ircSocket);
+
+ //lut.connect("#bastli_wasserstoffreakteur", -8554836);
+
+
+ ircClient.realName="Bastli";
+ ircClient.userName="bastli";
+ ircClient.nickName="bastli";
+ ircClient.onConnect~=((){
+ info("Connected to IRC on ",ircClient.serverAddress);
+ foreach(k;lut.ircChannels){
+ ircClient.join(k);
+ }
+ });
+ ircClient.onMessage~=(u,tt,m){
+ trace("IRC received message from "~u.nickName~" on "~tt~": "~m);
+ string msg=cast(string)m;
+ if(msg.skipOver("!bastli ")){
+ trace("Message is a command");
+ try{
+ auto split=msg.findSplit(" ");
+ switch(split[0]){
+ case "links":
+ ircClient.send(tt,lut.irc(tt).map!(a=>telegram.getChatName(a)~"("~a.to!string~")").join(", "));
+ break;
+ case "link":
+ //link(tt.idup, split[2].to!long);
+ break;
+ case "unlink":
+ unlink(tt.idup, split[2].to!long);
+ break;
+ case "list_telegram":
+ ircClient.send(tt, telegram_channels.byPair.map!(a=>a[1]~"("~a[0].to!string~")").join(", "));
+ break;
+ default:
+ ircClient.send(tt, "Unknown command");
+ break;
+ }
+ }
+ catch(Exception e){
+ ircClient.send(tt, "Error while executing command: "~e.msg);
+ warning(e.to!string);
+ }
+ }
+ else{
+ foreach(chatid; lut.irc(tt)){
+ telegram.send(chatid, "< "~u.nickName~"> "~m);
+ }
+ }
+ };
+
+
+ telegram.onMessage~=(j){
+ trace("Received Message "~j.toPrettyString());
+ auto msg=j["text"].str;
+ auto chatid=j["chat"]["id"].integer;
+ if(chatid !in telegram_channels){
+ if("title" in j["chat"]){
+ telegram_channels[chatid]=j["chat"]["title"].str;
+ }
+ else{
+ telegram_channels[chatid]=j["chat"]["username"].str;
+ }
+ }
+ if(msg.skipOver("/")){
+ trace("Bot command received");
+ try{
+ auto split=msg.findSplit(" ");
+ switch(split[0].findSplit("@")[0]){
+ case "link":
+ link(split[2], chatid);
+ ircClient.notice(split[2], "Linked with "~chatid.to!string);
+ break;
+ case "links":
+ telegram.send(chatid, lut.telegram(chatid).join(", "));
+ break;
+ case "unlink":
+ unlink(split[2], chatid);
+ ircClient.notice(split[2], "Unlinked with "~chatid.to!string);
+ break;
+ default:
+ //telegram.send(chatid, split[0]~": Unknown command");
+ break;
+ }
+ }
+ catch(Exception e){
+ telegram.send(chatid, "Error executing command: "~e.msg);
+ warning(e.to!string);
+ }
+ }
+ else{
+ auto irc_chans=lut.telegram(chatid);
+ if(irc_chans.length==0){
+ warning("Input on not-connected telegram-channel "~j["chat"].toPrettyString);
+ }
+ foreach(chan; irc_chans){
+ ircClient.send(chan, "< "~j["from"]["username"].str~"> "~msg);
+ }
+ }
+ };
+ }
+
+ void unlink(string irc, long telegram){
+ info("Unlinking ", irc, " from ", telegram);
+ lut.disconnect(irc, telegram);
+ if(ircClient.connected()){
+ if(lut.irc(irc).length==0){
+ ircClient.part(irc);
+ }
+ }
+ }
+
+ void link(string irc, long telegram){
+ info("Linking ", irc, " with ", telegram);
+ lut.connect(irc, telegram);
+ if(ircClient.connected()){
+ ircClient.join(irc);
+ }
+ }
+
+ void connect(){
+ info("Connecting to Telegram");
+ telegram.connect();
+ //ircClient.connect(new InternetAddress("127.0.0.1",6667));
+ info("Connecting to IRC");
+ ircClient.connect(ircAddress);
+ }
+
+ void setupEventloop(){
+ eventloop=ev_default_loop(0);
+
+ w_irc=Watcher(ircSocket,(revents){
+ trace("IRC Update");
+ if(ircClient.read()){
+ ev_io_stop(eventloop, &w_irc.io);
+ }
+ });
+
+ w_tele=Watcher(telegram.sock,(revents){
+ trace("Telegram Update");
+ Exception e;
+ bool res;
+ try res=telegram.read();
+ catch(TelegramException te) error(te);
+ if(res){
+ ev_io_stop(eventloop, &w_tele.io);
+ }
+ else{
+ telegram.triggerUpdates();
+ }
+ });
+
+ w_stdin=Watcher(stdin.fileno, (revents){
+ auto l=stdin.readln.chomp;
+ auto split=l.findSplit(" ");
+ switch(split[0]){
+ case "quit":
+ quit();
+ break;
+ case "link":
+ auto split2=split[2].findSplit(" ");
+ link(split2[0], split2[2].to!long);
+ break;
+ case "links":
+ foreach(i,tt; lut._irc.byPair){
+ foreach(t; tt){
+ writeln(i, " <-> ", t);
+ }
+ }
+ break;
+ case "unlink":
+ break;
+ default:
+ writeln(l, ": unknown command");
+ }
+
+ });
+ }
+
+ void quit(){
+ info("Quitting the bot");
+ ircClient.quit("Exiting gracefully");
+ disconnect();
+ stop();
+ }
+
+ void start(){
+ info("Starting the event-listeners");
+ telegram.triggerUpdates();
+ ev_io_start(eventloop, &w_irc.io);
+ ev_io_start(eventloop, &w_tele.io);
+ ev_io_start(eventloop, &w_stdin.io);
+ }
+
+ void stop(){
+ trace("Stopping the Eventloop");
+ ev_io_stop(eventloop, &w_tele.io);
+ ev_io_stop(eventloop, &w_irc.io);
+ ev_io_stop(eventloop, &w_stdin.io);
+ ev_break(eventloop, EVBREAK_ALL);
+ }
+
+ void run(){
+ info("Running the event-loop");
+ ev_run(eventloop,0);
+ }
+
+ void disconnect(){
+ telegram.disconnect();
+ }
+
+ void save(File f){
+ foreach(link; lut.links){
+ f.writeln(link[0], " ", link[1].to!string);
+ }
+ }
+ void load(File f){
+ foreach(l; f.byLine){
+ auto split=l.findSplit(" ");
+ link(split[0].idup, split[2].to!long);
+ }
+ }
+}
+
+
+int main(string[] args){
+ Bot b=Bot(args[1]);
+ b.initialize();
+ if(exists("savefile")){
+ auto f=File("savefile", "r");
+ b.load(f);
+ f.close();
+ }
+ b.connect();
+ scope(failure){
+ b.quit();
+ }
+ b.setupEventloop();
+ b.start();
+ b.run();
+ scope(exit){
+ auto f=File("savefile", "w+");
+ b.save(f);
+ f.close();
+ }
+
+ return 0;
+
+}
+