BastliBridge

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

irc.d (7637B)


      1 module bastlibridge.interfaces.irc;
      2 import bastlibridge.util;
      3 import bastlibridge.base;
      4 import bastlibridge.manager;
      5 import bastlibridge.command;
      6 import bastlibridge.interfaces.telegram;
      7 import std.socket;
      8 import std.exception;
      9 import std.typecons;
     10 import irc.client;
     11 import std.algorithm;
     12 import std.range;
     13 import std.json;
     14 import std.conv;
     15 import std.format;
     16 import std.array;
     17 import std.datetime : SysTime,Clock;
     18 import std.experimental.logger;
     19 
     20 alias waitForData=wait;
     21 	
     22 final class IRCMessage : Message{
     23 	import irc.client;
     24 	IrcUser user;
     25 	const(char)[] msg,channel;
     26 	SysTime dt;
     27 	
     28 	this(IrcUser u, in char[] msg, in char[] channel){
     29 		this.user=u;
     30 		this.msg=msg;
     31 		this.channel=channel;
     32 		dt=Clock.currTime();
     33 	}
     34 	
     35 	override const(char)[] getMessage(){
     36 		return msg;
     37 	}
     38 	
     39 	override const(char)[] userName(){
     40 		return user.nickName;
     41 	}
     42 	
     43 	override const(char)[] getChannelName(){
     44 		if(channel==(cast(IRC)source).ircClient.nickName){
     45 			return user.nickName;
     46 		}
     47 		else{
     48 			return channel;
     49 		}
     50 	}
     51 	
     52 	override SysTime getTime(){
     53 		return dt;
     54 	}
     55 	
     56 	override bool auth(){
     57 		bool* val=user.nickName in (cast(IRC)source).admins;
     58 		if(!val){
     59 			return false;
     60 		}
     61 		return *val;
     62 	}
     63 	
     64 	override void respond(in char[] r){
     65 		auto ep=cast(IRC)source;
     66 		const(char)[] chan=getChannelName();
     67 		trace("Sending response for ", msg, " on ", channel, " to ", chan, ": ", r);
     68 		ep.send(chan, r);
     69 	}
     70 }
     71 
     72 final class IRCChannel:Channel{
     73 	
     74 }
     75 
     76 final class IRC : QueuedEndpoint{
     77 	import ssl.socket;
     78 	import irc.url;
     79 	import irc.client;
     80 	static import irc.url;
     81 	
     82 	private IrcClient ircClient;
     83 	private Socket sock;
     84 	private string commandPrefix;
     85 	
     86 	Address addr;
     87 	
     88 	private __gshared bool shutdown=false;
     89 	
     90 	
     91 	private{
     92 	static immutable string unknown_username="Unknown";
     93 	string proxy_url;
     94 	string formatProxyURL(string file_id){
     95 		return proxy_url~file_id;
     96 	}
     97 	
     98 	string locationToOSMUrl(JSONValue loc){
     99 		return locationToOSMUrl(loc["latitude"].floating, loc["longitude"].floating);
    100 	}
    101 	string locationToOSMUrl(float lat, float lng){
    102 		return format!"https://www.openstreetmap.org/#map=%f/%f"(lat, lng);
    103 	}
    104 	
    105 	
    106 	static string TelegramUserToIRC(JSONValue user, TelegramMessage msg){
    107 		dchar mode=' ';
    108 		if(user["is_bot"].type==JSON_TYPE.TRUE){
    109 			mode='*';
    110 		}
    111 		return "<"~mode.to!string~msg.userName().idup~">";
    112 	}
    113 	
    114 	void TelegramToIRC(TelegramMessage msg, ref Appender!string app){
    115 		TelegramToIRC(msg, app, msg.json);
    116 	}
    117 	void TelegramToIRC(TelegramMessage msg, ref Appender!string app, JSONValue m){
    118 		string username="< "~unknown_username~">";
    119 		if(auto from="from" in m){
    120 			app~=TelegramUserToIRC(*from,msg);
    121 			app~=" ";
    122 		}
    123 		if(auto fwd="forward_from" in m){
    124 			app~=TelegramUserToIRC(*fwd,msg);
    125 			app~=" ";
    126 		}
    127 		foreach(t; ["text", "caption"]){
    128 			if(auto tv=t in m){
    129 				app~=(*tv).str;
    130 				app~=" ";
    131 			}
    132 		}
    133 		
    134 		if(auto photo="photo" in m){
    135 			string fid;
    136 			long size=long.min;
    137 			foreach(p; (*photo).array){
    138 				if(p["file_size"].integer>size){
    139 					size=p["file_size"].integer;
    140 					fid=p["file_id"].str;
    141 				}
    142 			}
    143 			app~=formatProxyURL(fid);
    144 			app~=" ";
    145 		}
    146 		
    147 		foreach(t; ["audio", "document", "sticker", "video", "voice"]){
    148 			if(auto tv=t in m){
    149 				app~=formatProxyURL((*tv)["file_id"].str);
    150 				app~=" ";
    151 			}
    152 		}
    153 		if(auto location="location" in m){
    154 			app~=locationToOSMUrl(*location);
    155 			app~=" ";
    156 		}
    157 		if("contact" in m){
    158 			auto c=m["contact"];
    159 			app~=c["first_name"].str;
    160 			app~=" ";
    161 			if("last_name" in c){
    162 				app~=c["last_name"].str;
    163 				app~=" ";
    164 			}
    165 			app~="(";
    166 			app~=c["phone_number"].str;
    167 			app~=") ";
    168 		}
    169 		if(auto venue="venue" in m){
    170 			app~=(*venue).str;
    171 			app~=locationToOSMUrl((*venue)["location"]);
    172 			app~=" ";
    173 		}
    174 		if(auto jv="reply_to_message" in m){
    175 			TelegramToIRC(msg, app, *jv);
    176 		}
    177 	}
    178 	string TelegramToIRC(TelegramMessage m){
    179 		Appender!string app;
    180 		
    181 		TelegramToIRC(m, app);
    182 		
    183 		return app.data[0..$-1];
    184 	}
    185 	}
    186 	
    187 	bool[string] admins;
    188 	
    189 	this(Manager m, string args){
    190 		super(m,args);
    191 		
    192 		string nick,username,realname;
    193 		bool have_addr=false;
    194 		ConnectionInfo info;
    195 		
    196 		foreach(arg; args.splitter(",")){
    197 			auto s=arg.findSplit("=");
    198 			switch(s[0]){
    199 				case "proxyurl": 
    200 					proxy_url=s[2];
    201 				break;
    202 				case "realname": 
    203 					realname=s[2];
    204 				break;
    205 				case "username": 
    206 					username=s[2];
    207 				break;
    208 				case "nick": 
    209 					nick=s[2];
    210 				break;
    211 				case "admin": 
    212 					admins[s[2]]=true;
    213 				break;
    214 				default:
    215 					info = irc.url.parse(cast(string)s[0]);
    216 					have_addr=true;
    217 				break;
    218 			}
    219 		}
    220 		
    221 		enforce(have_addr, "You have to provide an URL to connect to with irc://address");
    222 		enforce(nick, "You have to provide at least a nickname with nick=foo");
    223 		if(!username) username=nick;
    224 		if(!realname) realname=nick;
    225 		
    226 		this.commandPrefix=("!"~nick);
    227 		
    228 		addr = getAddress(info.address, info.port).front;
    229 		auto af=addr.addressFamily;
    230 		if(info.secure){
    231 			sock=new SslSocket(af);
    232 		}
    233 		else{
    234 			sock=new TcpSocket(af);
    235 		}
    236 		ircClient = new IrcClient(sock);
    237 		ircClient.nickName=nick;
    238 		ircClient.userName(username);
    239 		ircClient.realName(realname);
    240 		ircClient.onMessage~=&onInput;
    241 	}
    242 	
    243 	void onInput(IrcUser user, in char[] channel, in char[] msg){
    244 		trace("Got message from ",user.nickName, " in ", channel, ": ", msg);
    245 		heartbeat();
    246 		auto msg2=new IRCMessage(user, msg, channel);
    247 		msg2.source=this;
    248 		if(msg.startsWith(this.commandPrefix)){
    249 			trace("Is a command");
    250 			onCommand(msg2);
    251 		}
    252 		else{
    253 			trace("Is a message");
    254 			onMessage(msg2);
    255 		}
    256 	}
    257 	
    258 	void onMessage(IRCMessage msg){
    259 		auto chan=getChannel(msg.getChannelName());
    260 		if(!chan){
    261 			info("Got message on non-connected channel ", msg.channel);
    262 			return;
    263 		}
    264 		this.manager.distribute(Port(this,chan), msg);
    265 	}
    266 	
    267 	static CommandMachine ircCommands;
    268 	static this(){
    269 		ircCommands.add!((Message m, const(char)[] name){(cast(IRC)m.source).admins[name.idup]=true;})("addAdmin", CommandOptions(true));
    270 		ircCommands.add!((Message m, const(char)[] name){(cast(IRC)m.source).admins[name.idup]=false;})("removeAdmin", CommandOptions(true));
    271 	}
    272 	
    273 	void onCommand(IRCMessage msg){
    274 		auto s=msg.msg[commandPrefix.length+1..$].findSplit(" ");
    275 		if(ircCommands.available(s[0])){
    276 			ircCommands.execute(s[0], msg, s[2]);
    277 		}
    278 		else{
    279 			globalCommands.execute(s[0], msg, s[2]);
    280 		}
    281 	}
    282 	
    283 	override void open(){
    284 		info("Connecting to IRC on ", addr.toString);
    285 		ircClient.connect(addr);
    286 	}
    287 	
    288 	private void _join(string c){
    289 		if(c[0]=='#'){
    290 			trace("Joining irc channel ", c);
    291 			ircClient.join(c);
    292 		}
    293 		else{
    294 			trace("Not joining query ", c);
    295 		}
    296 	}
    297 	override Channel join(string c){
    298 		_join(c);
    299 		return new IRCChannel();
    300 	}
    301 	
    302 	override void part(Channel c){
    303 		auto ic=cast(IRCChannel)c;
    304 		ircClient.part(ic._name);
    305 	}
    306 	
    307 	override void sendMessageQueueless(scope Message m, Channel chan){
    308 		auto ichan=cast(IRCChannel)chan;
    309 		trace("Delivering message of type ",typeid(m)," to channel", ichan._name);
    310 		if(cast(TelegramMessage)m){
    311 			send(ichan, TelegramToIRC(cast(TelegramMessage)m));
    312 		}
    313 		else{
    314 			send(ichan, chain("< ",m.userName(), "> ", m.getMessage()));
    315 		}
    316 	}
    317 	
    318 	protected void send(T)(IRCChannel c, T text){
    319 		send(c._name, text);
    320 	}
    321 	
    322 	protected void send(T)(in char[] channel, T text){
    323 		ircClient.send(channel, text);
    324 	}
    325 	
    326 	override void run(){
    327 		import ssl.openssl;
    328 		loadOpenSSL(); //We have to do this once for every thread
    329 		info("Listening for IRC messages on");
    330 		ubyte[1] buf;
    331 		while(true){
    332 			waitForData(sock);
    333 			info("IRC message received");
    334 			if(shutdown)
    335 				break;
    336 			synchronized(mtx){
    337 				if(ircClient.read()){
    338 					break;
    339 				}
    340 				sendQueue();
    341 			}
    342 		}
    343 	}
    344 	override void stop(){
    345 		synchronized(mtx){
    346 			shutdown=true;
    347 			ircClient.quit("Planned shutdown");
    348 		}
    349 	}
    350 }