BigScreenBot.java 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827
  1. // Big Screen Bot
  2. //
  3. // Douglas Thrift
  4. //
  5. // BigScreenBot.java
  6. /* Copyright 2011 Douglas Thrift
  7. *
  8. * This file is part of Big Screen Bot.
  9. *
  10. * Big Screen Bot is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU General Public License as published by
  12. * the Free Software Foundation, either version 3 of the License, or
  13. * (at your option) any later version.
  14. *
  15. * Big Screen Bot is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU General Public License
  21. * along with Big Screen Bot. If not, see <http://www.gnu.org/licenses/>.
  22. */
  23. package net.douglasthrift.bigscreenbot;
  24. import java.io.IOException;
  25. import java.net.MalformedURLException;
  26. import java.net.UnknownHostException;
  27. import java.net.URL;
  28. import java.security.GeneralSecurityException;
  29. import java.util.ArrayList;
  30. import java.util.HashSet;
  31. import java.util.List;
  32. import java.util.Map;
  33. import java.util.TreeMap;
  34. import java.util.Set;
  35. import java.util.regex.Pattern;
  36. import javax.net.ssl.SSLSocketFactory;
  37. import com.google.polo.exception.PoloException;
  38. import com.google.polo.ssl.DummySSLSocketFactory;
  39. import org.apache.commons.cli.CommandLine;
  40. import org.apache.commons.cli.GnuParser;
  41. import org.apache.commons.cli.HelpFormatter;
  42. import org.apache.commons.cli.Options;
  43. import org.apache.commons.cli.ParseException;
  44. import org.apache.commons.lang3.StringUtils;
  45. import org.jibble.pircbot.Colors;
  46. import org.jibble.pircbot.IrcException;
  47. public class BigScreenBot extends Bot
  48. {
  49. private static final int CHANNEL = 0x1;
  50. private static final int PRIVATE = 0x2;
  51. private static final int BOTH = 0x3;
  52. private static enum Action { RECONNECT, RESTART, QUIT }
  53. private abstract class Command
  54. {
  55. private boolean admin;
  56. private int access;
  57. private String arguments, description;
  58. protected Command(boolean admin, int access, String arguments, String description)
  59. {
  60. this.admin = admin;
  61. this.access = access;
  62. this.arguments = arguments;
  63. this.description = description;
  64. }
  65. public abstract void execute(String channel, String sender, boolean admin, String argument);
  66. public int getAccess()
  67. {
  68. return access;
  69. }
  70. public String getArguments()
  71. {
  72. return arguments;
  73. }
  74. public String getDescription()
  75. {
  76. return description;
  77. }
  78. public boolean isAdmin()
  79. {
  80. return admin;
  81. }
  82. public boolean isChannel()
  83. {
  84. return (access & CHANNEL) != 0;
  85. }
  86. public boolean isPrivate()
  87. {
  88. return (access & PRIVATE) != 0;
  89. }
  90. }
  91. private boolean verbose;
  92. private Remote remote;
  93. private Settings settings = new Settings();
  94. private Set<String> channels;
  95. private List<Pattern> admins = new ArrayList<Pattern>();
  96. private Map<String, Pattern> bans = new TreeMap<String, Pattern>();
  97. private Map<String, Command> commands = new TreeMap<String, Command>();
  98. private Action action = Action.RECONNECT;
  99. private BigScreenBot(boolean verbose)
  100. {
  101. super();
  102. this.verbose = verbose;
  103. try
  104. {
  105. settings.load();
  106. remote = new Remote(settings);
  107. }
  108. catch (IOException exception)
  109. {
  110. error(exception, 1);
  111. }
  112. catch (GeneralSecurityException exception)
  113. {
  114. error(exception, 1);
  115. }
  116. setAutoNickChange(true);
  117. setFinger("Big Screen Bot");
  118. setMessageDelay(0);
  119. setVersion(String.format("Big Screen Bot (%1$s)", System.getProperty("os.name")));
  120. if (settings.getBooleanProperty("ssl", false))
  121. if (settings.getBooleanProperty("verify", true))
  122. setSocketFactory(SSLSocketFactory.getDefault());
  123. else
  124. try
  125. {
  126. setSocketFactory(DummySSLSocketFactory.fromKeyManagers(null));
  127. }
  128. catch (GeneralSecurityException exception)
  129. {
  130. error(exception, 1);
  131. }
  132. setLogin(System.getProperty("user.name"));
  133. setName(settings.getProperty("nick", "bigscreenbot"));
  134. setVerbose(verbose);
  135. channels = new HashSet<String>(settings.getListProperty("channels", new ArrayList<String>()));
  136. for (String admin : settings.getListProperty("admins"))
  137. admins.add(compileNickMask(admin));
  138. for (String ban : settings.getListProperty("bans", new ArrayList<String>()))
  139. bans.put(ban, compileNickMask(ban));
  140. commands.put("ban", new Command(true, PRIVATE, "[mask...]", "block nick masks from using commands")
  141. {
  142. @Override
  143. public void execute(String channel, String sender, boolean admin, String argument)
  144. {
  145. String[] arguments = StringUtils.split(argument);
  146. if (arguments.length == 0)
  147. {
  148. listBans(channel, sender);
  149. return;
  150. }
  151. synchronized (bans)
  152. {
  153. for (String ban : arguments)
  154. if (bans.put(ban, compileNickMask(ban)) == null)
  155. sendMessage(channel, sender, String.format("banned nick mask (\"%1$s\")", ban));
  156. else
  157. sendMessage(channel, sender, String.format("nick mask (\"%1$s\") already banned", ban));
  158. storeBans(channel, sender);
  159. }
  160. }
  161. });
  162. commands.put("googletv", new Command(false, BOTH, "url [device]", "fling url to a Google TV device")
  163. {
  164. @Override
  165. public void execute(final String channel, final String sender, boolean admin, String argument)
  166. {
  167. final String[] arguments = StringUtils.split(argument, null, 2);
  168. if (arguments.length == 0)
  169. {
  170. help(channel, sender, admin, "googletv");
  171. return;
  172. }
  173. try
  174. {
  175. if (!new URL(arguments[0]).getProtocol().matches("^https?$"))
  176. {
  177. invalidURL(channel, sender, arguments[0]);
  178. return;
  179. }
  180. }
  181. catch (MalformedURLException exception)
  182. {
  183. invalidURL(channel, sender, arguments[0]);
  184. return;
  185. }
  186. if (arguments.length == 2)
  187. sendMessage(channel, sender, String.format("flinging URL (\"%1$s\") to device (\"%2$s\")...", arguments[0], arguments[1]));
  188. else
  189. sendMessage(channel, sender, String.format("flinging URL (\"%1$s\") to device(s)...", arguments[0]));
  190. new Thread()
  191. {
  192. @Override
  193. public void run()
  194. {
  195. synchronized (remote)
  196. {
  197. if (arguments.length == 2)
  198. remote.fling(arguments[1], arguments[0]);
  199. else
  200. remote.fling(arguments[0]);
  201. }
  202. sendMessage(channel, sender, String.format("flung URL (\"%1$s\") to device(s)", arguments[0]));
  203. }
  204. }.start();
  205. }
  206. private void invalidURL(String channel, String sender, String url)
  207. {
  208. sendMessage(channel, sender, String.format("invalid URL (\"%1$s\")", url));
  209. }
  210. });
  211. commands.put("help", new Command(false, PRIVATE, "[command]", "show this help message")
  212. {
  213. @Override
  214. public void execute(String channel, String sender, boolean admin, String arguments)
  215. {
  216. String argument = null;
  217. Command command = null;
  218. try
  219. {
  220. argument = StringUtils.split(arguments, null, 2)[0].toLowerCase();
  221. if (argument.startsWith("!"))
  222. argument = argument.substring(1);
  223. command = commands.get(argument);
  224. }
  225. catch (ArrayIndexOutOfBoundsException exception) {}
  226. sendMessage(channel, sender, Colors.BOLD + String.format("%1$-11s %2$-23s %3$-15s %4$s", "command", "arguments", "access", "description") + Colors.NORMAL);
  227. if (command != null)
  228. help(channel, sender, admin, argument, command);
  229. else
  230. for (Map.Entry<String, Command> nameCommand : commands.entrySet())
  231. help(channel, sender, admin, nameCommand.getKey(), nameCommand.getValue());
  232. }
  233. private void help(String channel, String sender, boolean admin, String name, Command command)
  234. {
  235. boolean unavailable = command.isAdmin() && !admin;
  236. String access;
  237. switch (command.getAccess())
  238. {
  239. case CHANNEL:
  240. access = "channel"; break;
  241. case PRIVATE:
  242. access = "private"; break;
  243. case BOTH: default:
  244. access = "channel/private"; break;
  245. }
  246. sendMessage(channel, sender, (unavailable ? Colors.UNDERLINE : "") + String.format("%1$-11s %2$-23s %3$-15s %4$s", name, command.getArguments(), access, command.getDescription()) + (unavailable ? Colors.NORMAL : ""));
  247. }
  248. });
  249. commands.put("join", new Command(true, PRIVATE, "channel", "join a channel")
  250. {
  251. @Override
  252. public void execute(String channel, String sender, boolean admin, String arguments)
  253. {
  254. String argument;
  255. try
  256. {
  257. argument = StringUtils.split(arguments, null, 2)[0];
  258. }
  259. catch (ArrayIndexOutOfBoundsException exception)
  260. {
  261. help(channel, sender, admin, "join");
  262. return;
  263. }
  264. joinChannel(argument);
  265. synchronized (channels)
  266. {
  267. channels.add(argument);
  268. storeChannels(channel, sender);
  269. }
  270. sendMessage(channel, sender, String.format("joined channel (\"%1$s\")", argument));
  271. }
  272. });
  273. commands.put("pair", new Command(true, PRIVATE, "[device [code]]", "pair with a Google TV device")
  274. {
  275. @Override
  276. public void execute(final String channel, final String sender, boolean admin, String argument)
  277. {
  278. final String[] arguments = StringUtils.split(argument, null, 2);
  279. switch (arguments.length)
  280. {
  281. case 0:
  282. sendMessage(channel, sender, "searching for devices to pair with...");
  283. new Thread()
  284. {
  285. @Override
  286. public void run()
  287. {
  288. List<String> devices;
  289. synchronized (remote)
  290. {
  291. devices = remote.listDevices();
  292. }
  293. if (devices.isEmpty())
  294. {
  295. sendMessage(channel, sender, "there are no devices to pair with");
  296. return;
  297. }
  298. sendMessage(channel, sender, Colors.BOLD + "devices" + Colors.NORMAL);
  299. for (String device : devices)
  300. sendMessage(channel, sender, device);
  301. }
  302. }.start();
  303. break;
  304. case 1:
  305. sendMessage(channel, sender, String.format("starting to pair with device (\"%1$s\")...", arguments[0]));
  306. new Thread()
  307. {
  308. @Override
  309. public void run()
  310. {
  311. try
  312. {
  313. synchronized (remote)
  314. {
  315. remote.startPairDevice(arguments[0]);
  316. }
  317. }
  318. catch (MalformedURLException exception)
  319. {
  320. sendMessage(channel, sender, String.format("invalid device name (\"%1$s\")", arguments[0]));
  321. return;
  322. }
  323. catch (UnknownHostException exception)
  324. {
  325. sendMessage(channel, sender, String.format("could not find device (\"%1$s\")", arguments[0]));
  326. return;
  327. }
  328. catch (IOException exception)
  329. {
  330. error(channel, sender, exception);
  331. return;
  332. }
  333. catch (PoloException exception)
  334. {
  335. error(channel, sender, exception);
  336. return;
  337. }
  338. sendMessage(channel, sender, String.format("enter the code from the device (\"%1$s\") to finish pairing", arguments[0]));
  339. }
  340. }.start();
  341. break;
  342. default:
  343. sendMessage(channel, sender, String.format("finishing pairing with device (\"%1$s\")...", arguments[0]));
  344. new Thread()
  345. {
  346. @Override
  347. public void run()
  348. {
  349. synchronized (remote)
  350. {
  351. remote.finishPairDevice(arguments[0], arguments[1]);
  352. }
  353. sendMessage(channel, sender, String.format("paired with device (\"%1$s\")", arguments[0]));
  354. }
  355. }.start();
  356. }
  357. }
  358. });
  359. commands.put("part", new Command(true, PRIVATE, "channel [message]", "part from a channel")
  360. {
  361. @Override
  362. public void execute(String channel, String sender, boolean admin, String argument)
  363. {
  364. String[] arguments = StringUtils.split(argument, null, 2);
  365. if (arguments.length == 0)
  366. {
  367. help(channel, sender, admin, "part");
  368. return;
  369. }
  370. if (arguments.length == 2)
  371. partChannel(arguments[0], arguments[1]);
  372. else
  373. partChannel(arguments[0]);
  374. synchronized (channels)
  375. {
  376. channels.remove(arguments[0]);
  377. storeChannels(channel, sender);
  378. }
  379. sendMessage(channel, sender, String.format("parted channel (\"%1$s\")", arguments[0]));
  380. }
  381. });
  382. commands.put("quit", new Command(true, PRIVATE, "[message]", "quit and do not come back")
  383. {
  384. @Override
  385. public void execute(String channel, String sender, boolean admin, String argument)
  386. {
  387. synchronized (action)
  388. {
  389. action = Action.QUIT;
  390. }
  391. quitServer(!argument.isEmpty() ? argument : "oh no!");
  392. }
  393. });
  394. commands.put("restart", new Command(true, PRIVATE, "", "quit and join running more up to date code")
  395. {
  396. @Override
  397. public void execute(String channel, String sender, boolean admin, String argument)
  398. {
  399. synchronized (action)
  400. {
  401. action = Action.RESTART;
  402. }
  403. quitServer("restarting");
  404. }
  405. });
  406. commands.put("say", new Command(true, PRIVATE, "nick|channel message", "say message to nick or channel")
  407. {
  408. @Override
  409. public void execute(String channel, String sender, boolean admin, String argument)
  410. {
  411. String[] arguments = StringUtils.split(argument, null, 2);
  412. if (arguments.length != 2)
  413. {
  414. help(channel, sender, admin, "say");
  415. return;
  416. }
  417. if (arguments[0].equalsIgnoreCase(getNick()))
  418. {
  419. sendMessage(channel, sender, "nice try");
  420. return;
  421. }
  422. sendMessage(arguments[0], arguments[1]);
  423. sendMessage(channel, sender, String.format("successfully sent message (\"%1$s\") to nick/channel (\"%2$s\")", arguments[1], arguments[0]));
  424. }
  425. });
  426. commands.put("unban", new Command(true, PRIVATE, "[mask...]", "allow blocked nick masks to use commands again")
  427. {
  428. @Override
  429. public void execute(String channel, String sender, boolean admin, String argument)
  430. {
  431. String[] arguments = StringUtils.split(argument);
  432. if (arguments.length == 0)
  433. {
  434. listBans(channel, sender);
  435. return;
  436. }
  437. synchronized (bans)
  438. {
  439. for (String ban : arguments)
  440. if (bans.remove(ban) != null)
  441. sendMessage(channel, sender, String.format("unbanned nick mask (\"%1$s\")", ban));
  442. else
  443. sendMessage(channel, sender, String.format("nick mask (\"%1$s\") already unbanned", ban));
  444. storeBans(channel, sender);
  445. }
  446. }
  447. });
  448. try
  449. {
  450. connect(settings.getProperty("server"), settings.getIntegerProperty("port", 6667));
  451. }
  452. catch (IOException exception)
  453. {
  454. error(exception, 1);
  455. }
  456. catch (IrcException exception)
  457. {
  458. error(exception, 1);
  459. }
  460. }
  461. @Override
  462. public void dispose()
  463. {
  464. super.dispose();
  465. try
  466. {
  467. remote.close();
  468. }
  469. catch (IOException exception)
  470. {
  471. error(exception);
  472. }
  473. }
  474. public void sendMessage(String channel, String sender, String message)
  475. {
  476. sendMessage(channel != null ? channel : sender, message);
  477. }
  478. @Override
  479. protected void onConnect()
  480. {
  481. synchronized (channels)
  482. {
  483. for (String channel : channels)
  484. joinChannel(channel);
  485. }
  486. }
  487. @Override
  488. protected void onDisconnect()
  489. {
  490. synchronized (action)
  491. {
  492. switch (action)
  493. {
  494. case RESTART:
  495. dispose();
  496. System.exit(2);
  497. case QUIT:
  498. dispose();
  499. break;
  500. default:
  501. while (true)
  502. {
  503. try
  504. {
  505. reconnect();
  506. }
  507. catch (IOException exception)
  508. {
  509. continue;
  510. }
  511. catch (IrcException exception)
  512. {
  513. error(exception, 1);
  514. }
  515. break;
  516. }
  517. }
  518. }
  519. }
  520. @Override
  521. protected void onMessage(String channel, String sender, String login, String hostname, String message)
  522. {
  523. doCommandFromMessage(channel, sender, login, hostname, message);
  524. }
  525. @Override
  526. protected void onPrivateMessage(String sender, String login, String hostname, String message)
  527. {
  528. doCommandFromMessage(null, sender, login, hostname, message);
  529. }
  530. private void doCommandFromMessage(String channel, String sender, String login, String hostname, String message)
  531. {
  532. boolean admin = matchNickMasks(sender, login, hostname, admins);
  533. synchronized (bans)
  534. {
  535. if (!admin && (matchNickMasks(sender, login, hostname, bans.values()) || !isNickInChannels(sender)))
  536. return;
  537. }
  538. message = Colors.removeFormattingAndColors(message);
  539. String[] arguments = StringUtils.split(message, null, 2);
  540. String argument = "";
  541. Command command = null;
  542. if (arguments.length != 0)
  543. {
  544. if (channel == null)
  545. try
  546. {
  547. if (new URL(arguments[0]).getProtocol().matches("^https?$"))
  548. {
  549. commands.get("googletv").execute(channel, sender, admin, StringUtils.join(arguments, ' '));
  550. return;
  551. }
  552. }
  553. catch (MalformedURLException exception) {}
  554. argument = arguments[0].toLowerCase();
  555. if (argument.startsWith("!"))
  556. argument = argument.substring(1);
  557. command = commands.get(argument);
  558. }
  559. if (channel != null)
  560. {
  561. if (command == null)
  562. return;
  563. if (!admin && command.isAdmin())
  564. return;
  565. if (!command.isChannel())
  566. return;
  567. }
  568. else
  569. {
  570. if (command == null)
  571. {
  572. sendMessage(channel, sender, String.format("unknown command (\"%1$s\"); try \"help\"", argument));
  573. return;
  574. }
  575. if (!admin && command.isAdmin())
  576. {
  577. sendMessage(channel, sender, String.format("unauthorized command (\"%1$s\")", argument));
  578. return;
  579. }
  580. if (!command.isPrivate())
  581. {
  582. sendMessage(channel, sender, String.format("inappropriate command (\"%1$s\")", argument));
  583. return;
  584. }
  585. }
  586. command.execute(channel, sender, admin, arguments.length == 2 ? arguments[1] : "");
  587. }
  588. private void error(Exception exception)
  589. {
  590. if (verbose)
  591. exception.printStackTrace();
  592. else
  593. System.err.println("bigscreenbot: " + exception.getMessage());
  594. }
  595. private void error(Exception exception, int code)
  596. {
  597. error(exception);
  598. System.exit(code);
  599. }
  600. private void error(String channel, String sender, Exception exception)
  601. {
  602. error(exception);
  603. sendMessage(channel, sender, "an error occurred");
  604. }
  605. private void help(String channel, String sender, boolean admin, String command)
  606. {
  607. commands.get("help").execute(channel, sender, admin, command);
  608. }
  609. private void listBans(String channel, String sender)
  610. {
  611. synchronized (bans)
  612. {
  613. if (bans.isEmpty())
  614. {
  615. sendMessage(channel, sender, "there are no bans");
  616. return;
  617. }
  618. sendMessage(channel, sender, Colors.BOLD + "ban" + Colors.NORMAL);
  619. for (String ban : bans.keySet())
  620. sendMessage(channel, sender, ban);
  621. }
  622. }
  623. private void storeBans(String channel, String sender)
  624. {
  625. synchronized (bans)
  626. {
  627. synchronized (settings)
  628. {
  629. settings.setListProperty("bans", bans.keySet());
  630. try
  631. {
  632. settings.store();
  633. }
  634. catch (IOException exception)
  635. {
  636. error(channel, sender, exception);
  637. }
  638. }
  639. }
  640. }
  641. private void storeChannels(String channel, String sender)
  642. {
  643. synchronized (channels)
  644. {
  645. synchronized (settings)
  646. {
  647. settings.setListProperty("channels", channels);
  648. try
  649. {
  650. settings.store();
  651. }
  652. catch (IOException exception)
  653. {
  654. error(channel, sender, exception);
  655. }
  656. }
  657. }
  658. }
  659. public static void main(String[] args)
  660. {
  661. Options options = new Options();
  662. options.addOption("h", "help", false, "show this help message and exit");
  663. options.addOption("v", "verbose", false, "");
  664. CommandLine line = null;
  665. try
  666. {
  667. line = new GnuParser().parse(options, args);
  668. }
  669. catch (ParseException exception)
  670. {
  671. System.err.println("bigscreenbot: " + exception.getMessage());
  672. }
  673. if (line.hasOption('h'))
  674. {
  675. new HelpFormatter().printHelp("bigscreenbot", options, true);
  676. return;
  677. }
  678. new BigScreenBot(line.hasOption('v'));
  679. }
  680. }
  681. // vim: expandtab