You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Channel.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. /*
  2. * Copyright (c) 2006-2014 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc;
  23. import com.dmdirc.commandparser.CommandType;
  24. import com.dmdirc.commandparser.parsers.ChannelCommandParser;
  25. import com.dmdirc.events.ChannelActionEvent;
  26. import com.dmdirc.events.ChannelClosedEvent;
  27. import com.dmdirc.events.DisplayableEvent;
  28. import com.dmdirc.events.EventUtils;
  29. import com.dmdirc.interfaces.CommandController;
  30. import com.dmdirc.interfaces.Connection;
  31. import com.dmdirc.interfaces.GroupChat;
  32. import com.dmdirc.interfaces.NicklistListener;
  33. import com.dmdirc.interfaces.TopicChangeListener;
  34. import com.dmdirc.interfaces.config.ConfigChangeListener;
  35. import com.dmdirc.interfaces.config.ConfigProviderMigrator;
  36. import com.dmdirc.messages.MessageSinkManager;
  37. import com.dmdirc.parser.interfaces.ChannelClientInfo;
  38. import com.dmdirc.parser.interfaces.ChannelInfo;
  39. import com.dmdirc.parser.interfaces.ClientInfo;
  40. import com.dmdirc.ui.Colour;
  41. import com.dmdirc.ui.core.components.WindowComponent;
  42. import com.dmdirc.ui.input.TabCompleterFactory;
  43. import com.dmdirc.ui.input.TabCompletionType;
  44. import com.dmdirc.ui.messages.ColourManager;
  45. import com.dmdirc.ui.messages.Styliser;
  46. import com.dmdirc.util.URLBuilder;
  47. import com.dmdirc.util.annotations.factory.Factory;
  48. import com.dmdirc.util.annotations.factory.Unbound;
  49. import com.dmdirc.util.collections.ListenerList;
  50. import com.dmdirc.util.collections.RollingList;
  51. import com.google.common.base.Optional;
  52. import com.google.common.eventbus.EventBus;
  53. import java.util.ArrayList;
  54. import java.util.Arrays;
  55. import java.util.Collection;
  56. import java.util.Collections;
  57. import java.util.List;
  58. import java.util.Map;
  59. import javax.annotation.Nonnull;
  60. /**
  61. * The Channel class represents the client's view of the channel. It handles callbacks for channel
  62. * events from the parser, maintains the corresponding ChannelWindow, and handles user input for the
  63. * channel.
  64. */
  65. @Factory(inject = true, providers = true, singleton = true)
  66. public class Channel extends MessageTarget implements ConfigChangeListener, GroupChat {
  67. /** List of registered listeners. */
  68. private final ListenerList listenerList = new ListenerList();
  69. /** The parser's pChannel class. */
  70. private ChannelInfo channelInfo;
  71. /** The server this channel is on. */
  72. private final Server server;
  73. /** A list of previous topics we've seen. */
  74. private final RollingList<Topic> topics;
  75. /** Our event handler. */
  76. private final ChannelEventHandler eventHandler;
  77. /** The migrator to use to migrate our config provider. */
  78. private final ConfigProviderMigrator configMigrator;
  79. /** Whether we're in this channel or not. */
  80. private boolean isOnChannel;
  81. /** Whether we should send WHO requests for this channel. */
  82. private volatile boolean sendWho;
  83. /** Whether we should show mode prefixes in text. */
  84. private volatile boolean showModePrefix;
  85. /** Whether we should show colours in nicks. */
  86. private volatile boolean showColours;
  87. /**
  88. * Creates a new instance of Channel.
  89. *
  90. * @param newServer The server object that this channel belongs to
  91. * @param newChannelInfo The parser's channel object that corresponds to this channel
  92. * @param configMigrator The config migrator which provides the config for this channel.
  93. * @param tabCompleterFactory The factory to use to create tab completers.
  94. * @param commandController The controller to load commands from.
  95. * @param messageSinkManager The sink manager to use to despatch messages.
  96. * @param urlBuilder The URL builder to use when finding icons.
  97. * @param eventBus The bus to despatch events onto.
  98. */
  99. public Channel(
  100. @Unbound final Server newServer,
  101. @Unbound final ChannelInfo newChannelInfo,
  102. @Unbound final ConfigProviderMigrator configMigrator,
  103. final TabCompleterFactory tabCompleterFactory,
  104. final CommandController commandController,
  105. final MessageSinkManager messageSinkManager,
  106. final URLBuilder urlBuilder,
  107. final EventBus eventBus) {
  108. super(newServer, "channel-inactive", newChannelInfo.getName(),
  109. Styliser.stipControlCodes(newChannelInfo.getName()),
  110. configMigrator.getConfigProvider(),
  111. new ChannelCommandParser(newServer, commandController),
  112. tabCompleterFactory.getTabCompleter(newServer.getTabCompleter(),
  113. configMigrator.getConfigProvider(), CommandType.TYPE_CHANNEL,
  114. CommandType.TYPE_CHAT),
  115. messageSinkManager,
  116. urlBuilder,
  117. newServer.getEventBus(),
  118. Arrays.asList(WindowComponent.TEXTAREA.getIdentifier(),
  119. WindowComponent.INPUTFIELD.getIdentifier(),
  120. WindowComponent.TOPICBAR.getIdentifier(),
  121. WindowComponent.USERLIST.getIdentifier()));
  122. this.configMigrator = configMigrator;
  123. this.channelInfo = newChannelInfo;
  124. this.server = newServer;
  125. getConfigManager().addChangeListener("channel", this);
  126. getConfigManager().addChangeListener("ui", "shownickcoloursintext", this);
  127. topics = new RollingList<>(getConfigManager().getOptionInt("channel",
  128. "topichistorysize"));
  129. sendWho = getConfigManager().getOptionBool("channel", "sendwho");
  130. showModePrefix = getConfigManager().getOptionBool("channel", "showmodeprefix");
  131. showColours = getConfigManager().getOptionBool("ui", "shownickcoloursintext");
  132. eventHandler = new ChannelEventHandler(this, getEventBus());
  133. registerCallbacks();
  134. updateTitle();
  135. selfJoin();
  136. }
  137. public ChannelInfo getChannelInfo() {
  138. return channelInfo;
  139. }
  140. @Override
  141. public boolean isOnChannel() {
  142. return isOnChannel;
  143. }
  144. /**
  145. * Registers callbacks with the parser for this channel.
  146. */
  147. private void registerCallbacks() {
  148. eventHandler.registerCallbacks();
  149. configMigrator.migrate(server.getProtocol(), server.getIrcd(),
  150. server.getNetwork(), server.getAddress(), channelInfo.getName());
  151. }
  152. @Override
  153. public void sendLine(final String line) {
  154. if (server.getState() != ServerState.CONNECTED
  155. || server.getParser().getChannel(channelInfo.getName()) == null) {
  156. // We're not in the channel/connected to the server
  157. return;
  158. }
  159. final ClientInfo me = server.getParser().getLocalClient();
  160. final String[] details = getDetails(channelInfo.getChannelClient(me));
  161. for (String part : splitLine(line)) {
  162. if (!part.isEmpty()) {
  163. final DisplayableEvent event = new ChannelActionEvent(this,
  164. channelInfo.getChannelClient(me), part);
  165. final String format = EventUtils.postDisplayable(getEventBus(), event,
  166. "channelSelfMessage");
  167. addLine(format, details[0], details[1], details[2], details[3], part, channelInfo);
  168. channelInfo.sendMessage(part);
  169. }
  170. }
  171. }
  172. @Override
  173. public int getMaxLineLength() {
  174. return server.getState() == ServerState.CONNECTED
  175. ? server.getParser().getMaxLength("PRIVMSG", getChannelInfo().getName())
  176. : -1;
  177. }
  178. @Override
  179. public void sendAction(final String action) {
  180. if (server.getState() != ServerState.CONNECTED
  181. || server.getParser().getChannel(channelInfo.getName()) == null) {
  182. // We're not on the server/channel
  183. return;
  184. }
  185. final ClientInfo me = server.getParser().getLocalClient();
  186. final String[] details = getDetails(channelInfo.getChannelClient(me));
  187. if (server.getParser().getMaxLength("PRIVMSG", getChannelInfo().getName())
  188. <= action.length()) {
  189. addLine("actionTooLong", action.length());
  190. } else {
  191. final DisplayableEvent event = new ChannelActionEvent(this,
  192. channelInfo.getChannelClient(me), action);
  193. final String format = EventUtils.postDisplayable(getEventBus(), event,
  194. "channelSelfAction");
  195. addLine(format, details[0], details[1], details[2], details[3], action, channelInfo);
  196. channelInfo.sendAction(action);
  197. }
  198. }
  199. /**
  200. * Sets this object's ChannelInfo reference to the one supplied. This only needs to be done if
  201. * the channel window (and hence this channel object) has stayed open while the user has been
  202. * out of the channel.
  203. *
  204. * @param newChannelInfo The new ChannelInfo object
  205. */
  206. public void setChannelInfo(final ChannelInfo newChannelInfo) {
  207. channelInfo = newChannelInfo;
  208. registerCallbacks();
  209. }
  210. /**
  211. * Called when we join this channel. Just needs to output a message.
  212. */
  213. public void selfJoin() {
  214. isOnChannel = true;
  215. final ClientInfo me = server.getParser().getLocalClient();
  216. addLine("channelSelfJoin", "", me.getNickname(), me.getUsername(),
  217. me.getHostname(), channelInfo.getName());
  218. checkWho();
  219. setIcon("channel");
  220. server.removeInvites(channelInfo.getName());
  221. }
  222. /**
  223. * Updates the title of the channel window, and of the main window if appropriate.
  224. */
  225. private void updateTitle() {
  226. String temp = Styliser.stipControlCodes(channelInfo.getName());
  227. if (!channelInfo.getTopic().isEmpty()) {
  228. temp += " - " + Styliser.stipControlCodes(channelInfo.getTopic());
  229. }
  230. setTitle(temp);
  231. }
  232. @Override
  233. public void join() {
  234. server.getParser().joinChannel(channelInfo.getName());
  235. }
  236. @Override
  237. public void part(final String reason) {
  238. channelInfo.part(reason);
  239. resetWindow();
  240. }
  241. @Override
  242. public void retrieveListModes() {
  243. channelInfo.requestListModes();
  244. }
  245. /**
  246. * Resets the window state after the client has left a channel.
  247. */
  248. public void resetWindow() {
  249. isOnChannel = false;
  250. setIcon("channel-inactive");
  251. listenerList.getCallable(NicklistListener.class)
  252. .clientListUpdated(Collections.<ChannelClientInfo>emptyList());
  253. }
  254. @Override
  255. public void close() {
  256. super.close();
  257. // Remove any callbacks or listeners
  258. eventHandler.unregisterCallbacks();
  259. if (server.getParser() != null) {
  260. server.getParser().getCallbackManager().delAllCallback(eventHandler);
  261. }
  262. // Trigger any actions neccessary
  263. if (isOnChannel) {
  264. part(getConfigManager().getOption("general", "partmessage"));
  265. }
  266. // Trigger action for the window closing
  267. getEventBus().post(new ChannelClosedEvent(this));
  268. // Inform any parents that the window is closing
  269. server.delChannel(channelInfo.getName());
  270. }
  271. /**
  272. * Called every {general.whotime} seconds to check if the channel needs to send a who request.
  273. */
  274. public void checkWho() {
  275. if (isOnChannel && sendWho) {
  276. channelInfo.sendWho();
  277. }
  278. }
  279. /**
  280. * Adds a ChannelClient to this Channel.
  281. *
  282. * @param client The client to be added
  283. */
  284. public void addClient(final ChannelClientInfo client) {
  285. listenerList.getCallable(NicklistListener.class).clientAdded(client);
  286. getTabCompleter().addEntry(TabCompletionType.CHANNEL_NICK,
  287. client.getClient().getNickname());
  288. }
  289. /**
  290. * Removes the specified ChannelClient from this channel.
  291. *
  292. * @param client The client to be removed
  293. */
  294. public void removeClient(final ChannelClientInfo client) {
  295. listenerList.getCallable(NicklistListener.class).clientRemoved(client);
  296. getTabCompleter().removeEntry(TabCompletionType.CHANNEL_NICK,
  297. client.getClient().getNickname());
  298. if (client.getClient().equals(server.getParser().getLocalClient())) {
  299. resetWindow();
  300. }
  301. }
  302. /**
  303. * Replaces the list of known clients on this channel with the specified one.
  304. *
  305. * @param clients The list of clients to use
  306. */
  307. public void setClients(final Collection<ChannelClientInfo> clients) {
  308. listenerList.getCallable(NicklistListener.class).clientListUpdated(clients);
  309. getTabCompleter().clear(TabCompletionType.CHANNEL_NICK);
  310. for (ChannelClientInfo client : clients) {
  311. getTabCompleter().addEntry(TabCompletionType.CHANNEL_NICK,
  312. client.getClient().getNickname());
  313. }
  314. }
  315. /**
  316. * Renames a client that is in this channel.
  317. *
  318. * @param oldName The old nickname of the client
  319. * @param newName The new nickname of the client
  320. */
  321. public void renameClient(final String oldName, final String newName) {
  322. getTabCompleter().removeEntry(TabCompletionType.CHANNEL_NICK, oldName);
  323. getTabCompleter().addEntry(TabCompletionType.CHANNEL_NICK, newName);
  324. refreshClients();
  325. }
  326. /**
  327. * Refreshes the list of clients stored by this channel. Should be called when (visible) user
  328. * modes or nicknames change.
  329. */
  330. public void refreshClients() {
  331. if (!isOnChannel) {
  332. return;
  333. }
  334. listenerList.getCallable(NicklistListener.class).clientListUpdated();
  335. }
  336. /**
  337. * Returns a string containing the most important mode for the specified client.
  338. *
  339. * @param channelClient The channel client to check.
  340. *
  341. * @return A string containing the most important mode, or an empty string if there are no
  342. * (known) modes.
  343. */
  344. private String getModes(final ChannelClientInfo channelClient) {
  345. if (channelClient == null || !showModePrefix) {
  346. return "";
  347. } else {
  348. return channelClient.getImportantModePrefix();
  349. }
  350. }
  351. @Override
  352. public void configChanged(final String domain, final String key) {
  353. switch (key) {
  354. case "sendwho":
  355. sendWho = getConfigManager().getOptionBool("channel", "sendwho");
  356. break;
  357. case "showmodeprefix":
  358. showModePrefix = getConfigManager().getOptionBool("channel", "showmodeprefix");
  359. break;
  360. case "shownickcoloursintext":
  361. showColours = getConfigManager().getOptionBool("ui", "shownickcoloursintext");
  362. break;
  363. }
  364. }
  365. /**
  366. * Returns a string[] containing the nickname/ident/host of a channel client.
  367. *
  368. * @param client The channel client to check
  369. *
  370. * @return A string[] containing displayable components
  371. */
  372. private String[] getDetails(final ChannelClientInfo client) {
  373. if (client == null) {
  374. // WTF?
  375. throw new UnsupportedOperationException("getDetails called with"
  376. + " null ChannelClientInfo");
  377. }
  378. final String[] res = new String[]{
  379. getModes(client),
  380. Styliser.CODE_NICKNAME + client.getClient().getNickname() + Styliser.CODE_NICKNAME,
  381. client.getClient().getUsername(),
  382. client.getClient().getHostname(),};
  383. if (showColours) {
  384. final Map<?, ?> map = client.getMap();
  385. if (map.containsKey(ChannelClientProperty.TEXT_FOREGROUND)) {
  386. String prefix;
  387. if (map.containsKey(ChannelClientProperty.TEXT_BACKGROUND)) {
  388. prefix = "," + ColourManager.getHex((Colour) map.get(
  389. ChannelClientProperty.TEXT_BACKGROUND));
  390. } else {
  391. prefix = Styliser.CODE_HEXCOLOUR + ColourManager.getHex((Colour) map.get(
  392. ChannelClientProperty.TEXT_FOREGROUND));
  393. }
  394. res[1] = prefix + res[1] + Styliser.CODE_HEXCOLOUR;
  395. }
  396. }
  397. return res;
  398. }
  399. @Override
  400. protected boolean processNotificationArg(final Object arg, final List<Object> args) {
  401. if (arg instanceof ClientInfo) {
  402. // Format ClientInfos
  403. final ClientInfo clientInfo = (ClientInfo) arg;
  404. args.add(clientInfo.getNickname());
  405. args.add(clientInfo.getUsername());
  406. args.add(clientInfo.getHostname());
  407. return true;
  408. } else if (arg instanceof ChannelClientInfo) {
  409. // Format ChannelClientInfos
  410. final ChannelClientInfo clientInfo = (ChannelClientInfo) arg;
  411. args.addAll(Arrays.asList(getDetails(clientInfo)));
  412. return true;
  413. } else if (arg instanceof Topic) {
  414. // Format topics
  415. args.add("");
  416. args.addAll(Arrays.asList(server.parseHostmask(((Topic) arg).getClient())));
  417. args.add(((Topic) arg).getTopic());
  418. args.add(((Topic) arg).getTime() * 1000);
  419. return true;
  420. } else {
  421. // Everything else - default formatting
  422. return super.processNotificationArg(arg, args);
  423. }
  424. }
  425. @Override
  426. protected void modifyNotificationArgs(final List<Object> actionArgs,
  427. final List<Object> messageArgs) {
  428. messageArgs.add(channelInfo.getName());
  429. }
  430. // ---------------------------------------------------- TOPIC HANDLING -----
  431. /**
  432. * Adds the specified topic to this channel's topic list.
  433. *
  434. * @param topic The topic to be added.
  435. */
  436. public void addTopic(final Topic topic) {
  437. synchronized (topics) {
  438. topics.add(topic);
  439. }
  440. updateTitle();
  441. new Thread(new Runnable() {
  442. @Override
  443. public void run() {
  444. listenerList.getCallable(TopicChangeListener.class)
  445. .topicChanged(Channel.this, topic);
  446. }
  447. }, "Topic change listener runner").start();
  448. }
  449. @Override
  450. public List<Topic> getTopics() {
  451. synchronized (topics) {
  452. return new ArrayList<>(topics.getList());
  453. }
  454. }
  455. @Override
  456. public Optional<Topic> getCurrentTopic() {
  457. synchronized (topics) {
  458. if (topics.getList().isEmpty()) {
  459. return Optional.absent();
  460. } else {
  461. return Optional.of(topics.get(topics.getList().size() - 1));
  462. }
  463. }
  464. }
  465. // ------------------------------------------ PARSER METHOD DELEGATION -----
  466. @Override
  467. public void setTopic(final String topic) {
  468. channelInfo.setTopic(topic);
  469. }
  470. @Override
  471. public int getMaxTopicLength() {
  472. return server.getParser().getMaxTopicLength();
  473. }
  474. @Override
  475. public void addNicklistListener(final NicklistListener listener) {
  476. listenerList.add(NicklistListener.class, listener);
  477. }
  478. @Override
  479. public void removeNicklistListener(final NicklistListener listener) {
  480. listenerList.remove(NicklistListener.class, listener);
  481. }
  482. @Override
  483. public void addTopicChangeListener(final TopicChangeListener listener) {
  484. listenerList.add(TopicChangeListener.class, listener);
  485. }
  486. @Override
  487. public void removeTopicChangeListener(final TopicChangeListener listener) {
  488. listenerList.remove(TopicChangeListener.class, listener);
  489. }
  490. @Override
  491. @Nonnull
  492. public Connection getConnection() {
  493. return server;
  494. }
  495. }