BigScreenBot.java 24 KB

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