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 }