BigScreenBot.java 24 KB

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