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 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. /*
  2. * Copyright (c) 2006-2015 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.config.ConfigBinding;
  25. import com.dmdirc.events.ChannelClosedEvent;
  26. import com.dmdirc.events.ChannelSelfActionEvent;
  27. import com.dmdirc.events.ChannelSelfJoinEvent;
  28. import com.dmdirc.events.ChannelSelfMessageEvent;
  29. import com.dmdirc.events.CommandErrorEvent;
  30. import com.dmdirc.events.DisplayProperty;
  31. import com.dmdirc.events.NickListClientAddedEvent;
  32. import com.dmdirc.events.NickListClientRemovedEvent;
  33. import com.dmdirc.events.NickListClientsChangedEvent;
  34. import com.dmdirc.events.NickListUpdatedEvent;
  35. import com.dmdirc.interfaces.Connection;
  36. import com.dmdirc.interfaces.GroupChat;
  37. import com.dmdirc.interfaces.GroupChatUser;
  38. import com.dmdirc.interfaces.User;
  39. import com.dmdirc.interfaces.WindowModel;
  40. import com.dmdirc.interfaces.config.ConfigProviderMigrator;
  41. import com.dmdirc.parser.common.ChannelListModeItem;
  42. import com.dmdirc.parser.interfaces.ChannelClientInfo;
  43. import com.dmdirc.parser.interfaces.ChannelInfo;
  44. import com.dmdirc.parser.interfaces.Parser;
  45. import com.dmdirc.ui.core.components.WindowComponent;
  46. import com.dmdirc.ui.input.TabCompleterFactory;
  47. import com.dmdirc.ui.input.TabCompletionType;
  48. import com.dmdirc.ui.messages.BackBufferFactory;
  49. import com.dmdirc.ui.messages.Styliser;
  50. import com.dmdirc.ui.messages.sink.MessageSinkManager;
  51. import com.dmdirc.util.colours.Colour;
  52. import com.dmdirc.util.colours.ColourUtils;
  53. import com.google.common.collect.EvictingQueue;
  54. import java.util.ArrayList;
  55. import java.util.Arrays;
  56. import java.util.Collection;
  57. import java.util.Collections;
  58. import java.util.List;
  59. import java.util.Optional;
  60. import java.util.Queue;
  61. import java.util.stream.Collectors;
  62. import javax.annotation.Nullable;
  63. /**
  64. * The Channel class represents the client's view of the channel. It handles callbacks for channel
  65. * events from the parser, maintains the corresponding ChannelWindow, and handles user input for the
  66. * channel.
  67. */
  68. public class Channel extends FrameContainer implements GroupChat {
  69. /** The parser's pChannel class. */
  70. private ChannelInfo channelInfo;
  71. /** The connection this channel is on. */
  72. private final Connection connection;
  73. /** A list of previous topics we've seen. */
  74. private final Queue<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. /** Manager used to retrieve {@link GroupChatUser}s */
  80. private final GroupChatUserManager groupChatUserManager;
  81. /** Whether we're in this channel or not. */
  82. private boolean isOnChannel;
  83. /** Whether we should show mode prefixes in text. */
  84. @ConfigBinding(domain = "channel", key = "showmodeprefix")
  85. private volatile boolean showModePrefix;
  86. /** Whether we should show colours in nicks. */
  87. @ConfigBinding(domain = "ui", key = "shownickcoloursintext")
  88. private volatile boolean showColours;
  89. /**
  90. * Creates a new instance of Channel.
  91. *
  92. * @param connection The connection object that this channel belongs to
  93. * @param newChannelInfo The parser's channel object that corresponds to this channel
  94. * @param configMigrator The config migrator which provides the config for this channel.
  95. * @param tabCompleterFactory The factory to use to create tab completers.
  96. * @param messageSinkManager The sink manager to use to despatch messages.
  97. */
  98. public Channel(
  99. final Connection connection,
  100. final ChannelInfo newChannelInfo,
  101. final ConfigProviderMigrator configMigrator,
  102. final TabCompleterFactory tabCompleterFactory,
  103. final MessageSinkManager messageSinkManager,
  104. final BackBufferFactory backBufferFactory,
  105. final GroupChatUserManager groupChatUserManager) {
  106. super(connection.getWindowModel(), "channel-inactive",
  107. newChannelInfo.getName(),
  108. Styliser.stipControlCodes(newChannelInfo.getName()),
  109. configMigrator.getConfigProvider(),
  110. backBufferFactory,
  111. tabCompleterFactory.getTabCompleter(connection.getWindowModel().getTabCompleter(),
  112. configMigrator.getConfigProvider(), CommandType.TYPE_CHANNEL,
  113. CommandType.TYPE_CHAT),
  114. messageSinkManager,
  115. connection.getWindowModel().getEventBus(),
  116. Arrays.asList(WindowComponent.TEXTAREA.getIdentifier(),
  117. WindowComponent.INPUTFIELD.getIdentifier(),
  118. WindowComponent.TOPICBAR.getIdentifier(),
  119. WindowComponent.USERLIST.getIdentifier()));
  120. this.configMigrator = configMigrator;
  121. this.channelInfo = newChannelInfo;
  122. this.connection = connection;
  123. this.groupChatUserManager = groupChatUserManager;
  124. getConfigManager().getBinder().bind(this, Channel.class);
  125. topics = EvictingQueue.create(
  126. getConfigManager().getOptionInt("channel", "topichistorysize"));
  127. eventHandler = new ChannelEventHandler(this, getEventBus(), groupChatUserManager);
  128. initBackBuffer();
  129. registerCallbacks();
  130. updateTitle();
  131. selfJoin();
  132. }
  133. public ChannelInfo getChannelInfo() {
  134. return channelInfo;
  135. }
  136. @Override
  137. public boolean isOnChannel() {
  138. return isOnChannel;
  139. }
  140. /**
  141. * Registers callbacks with the parser for this channel.
  142. */
  143. private void registerCallbacks() {
  144. eventHandler.registerCallbacks();
  145. configMigrator.migrate(connection.getProtocol(), connection.getIrcd(), connection.getNetwork(),
  146. connection.getAddress(), channelInfo.getName());
  147. }
  148. @Override
  149. public void sendLine(final String line) {
  150. if (connection.getState() != ServerState.CONNECTED
  151. || connection.getParser().get().getChannel(channelInfo.getName()) == null) {
  152. // We're not in the channel/connected to the server
  153. return;
  154. }
  155. final GroupChatUser me = getUser(connection.getLocalUser().get()).get();
  156. splitLine(line).stream().filter(part -> !part.isEmpty()).forEach(part -> {
  157. getEventBus().publishAsync(new ChannelSelfMessageEvent(this, me, part));
  158. channelInfo.sendMessage(part);
  159. });
  160. }
  161. @Override
  162. public int getMaxLineLength() {
  163. return connection.getState() == ServerState.CONNECTED
  164. ? connection.getParser().get().getMaxLength("PRIVMSG", getChannelInfo().getName())
  165. : -1;
  166. }
  167. @Override
  168. public void sendAction(final String action) {
  169. if (connection.getState() != ServerState.CONNECTED
  170. || connection.getParser().get().getChannel(channelInfo.getName()) == null) {
  171. // We're not on the server/channel
  172. return;
  173. }
  174. if (connection.getParser().get().getMaxLength("PRIVMSG", getChannelInfo().getName())
  175. <= action.length()) {
  176. getEventBus().publishAsync(new CommandErrorEvent(this,
  177. "Warning: action too long to be sent"));
  178. } else {
  179. final GroupChatUser me = getUser(connection.getLocalUser().get()).get();
  180. getEventBus().publishAsync(new ChannelSelfActionEvent(this, me, action));
  181. channelInfo.sendAction(action);
  182. }
  183. }
  184. /**
  185. * Sets this object's ChannelInfo reference to the one supplied. This only needs to be done if
  186. * the channel window (and hence this channel object) has stayed open while the user has been
  187. * out of the channel.
  188. *
  189. * @param newChannelInfo The new ChannelInfo object
  190. */
  191. public void setChannelInfo(final ChannelInfo newChannelInfo) {
  192. channelInfo = newChannelInfo;
  193. registerCallbacks();
  194. }
  195. /**
  196. * Called when we join this channel. Just needs to output a message.
  197. */
  198. public void selfJoin() {
  199. isOnChannel = true;
  200. final User me = connection.getLocalUser().get();
  201. getEventBus().publishAsync(new ChannelSelfJoinEvent(this, me));
  202. setIcon("channel");
  203. connection.getInviteManager().removeInvites(channelInfo.getName());
  204. }
  205. /**
  206. * Updates the title of the channel window, and of the main window if appropriate.
  207. */
  208. private void updateTitle() {
  209. String temp = Styliser.stipControlCodes(channelInfo.getName());
  210. if (!channelInfo.getTopic().isEmpty()) {
  211. temp += " - " + Styliser.stipControlCodes(channelInfo.getTopic());
  212. }
  213. setTitle(temp);
  214. }
  215. @Override
  216. public void join() {
  217. connection.getParser().get().joinChannel(channelInfo.getName());
  218. }
  219. @Override
  220. public void part(final String reason) {
  221. channelInfo.part(reason);
  222. resetWindow();
  223. }
  224. @Override
  225. public void retrieveListModes() {
  226. channelInfo.requestListModes();
  227. }
  228. /**
  229. * Resets the window state after the client has left a channel.
  230. */
  231. public void resetWindow() {
  232. isOnChannel = false;
  233. setIcon("channel-inactive");
  234. // Needs to be published synchronously so that nicklists are cleared before the parser
  235. // is disconnected (which happens synchronously after this method returns).
  236. getEventBus().publish(
  237. new NickListClientsChangedEvent(this, Collections.<GroupChatUser>emptyList()));
  238. }
  239. @Override
  240. public void close() {
  241. super.close();
  242. // Remove any callbacks or listeners
  243. eventHandler.unregisterCallbacks();
  244. getConfigManager().getBinder().unbind(this);
  245. connection.getParser().map(Parser::getCallbackManager)
  246. .ifPresent(cm -> cm.unsubscribe(eventHandler));
  247. // Trigger any actions neccessary
  248. if (isOnChannel && connection.getState() != ServerState.CLOSING) {
  249. part(getConfigManager().getOption("general", "partmessage"));
  250. }
  251. // Trigger action for the window closing
  252. getEventBus().publish(new ChannelClosedEvent(this));
  253. }
  254. /**
  255. * Adds a ChannelClient to this Channel.
  256. *
  257. * @param client The client to be added
  258. */
  259. public void addClient(final GroupChatUser client) {
  260. getEventBus().publishAsync(new NickListClientAddedEvent(this, client));
  261. getTabCompleter().addEntry(TabCompletionType.CHANNEL_NICK, client.getNickname());
  262. }
  263. /**
  264. * Removes the specified ChannelClient from this channel.
  265. *
  266. * @param client The client to be removed
  267. */
  268. public void removeClient(final GroupChatUser client) {
  269. getEventBus().publishAsync(new NickListClientRemovedEvent(this, client));
  270. getTabCompleter().removeEntry(TabCompletionType.CHANNEL_NICK, client.getNickname());
  271. if (client.getUser().equals(connection.getLocalUser().orElse(null))) {
  272. resetWindow();
  273. }
  274. }
  275. /**
  276. * Replaces the list of known clients on this channel with the specified one.
  277. *
  278. * @param clients The list of clients to use
  279. */
  280. public void setClients(final Collection<GroupChatUser> clients) {
  281. getEventBus().publishAsync(new NickListClientsChangedEvent(this, clients));
  282. getTabCompleter().clear(TabCompletionType.CHANNEL_NICK);
  283. getTabCompleter().addEntries(TabCompletionType.CHANNEL_NICK,
  284. clients.stream().map(GroupChatUser::getNickname).collect(Collectors.toList()));
  285. }
  286. /**
  287. * Renames a client that is in this channel.
  288. *
  289. * @param oldName The old nickname of the client
  290. * @param newName The new nickname of the client
  291. */
  292. public void renameClient(final String oldName, final String newName) {
  293. getTabCompleter().removeEntry(TabCompletionType.CHANNEL_NICK, oldName);
  294. getTabCompleter().addEntry(TabCompletionType.CHANNEL_NICK, newName);
  295. refreshClients();
  296. }
  297. @Override
  298. public void refreshClients() {
  299. if (!isOnChannel) {
  300. return;
  301. }
  302. getEventBus().publishAsync(new NickListUpdatedEvent(this));
  303. }
  304. /**
  305. * Returns a string containing the most important mode for the specified client.
  306. *
  307. * @param user The channel client to check.
  308. *
  309. * @return A string containing the most important mode, or an empty string if there are no
  310. * (known) modes.
  311. */
  312. private String getModes(final GroupChatUser user) {
  313. if (user == null || !showModePrefix) {
  314. return "";
  315. } else {
  316. return user.getImportantMode();
  317. }
  318. }
  319. /**
  320. * Returns a string[] containing the nickname/ident/host of a channel client.
  321. *
  322. *
  323. *
  324. * @param client The channel client to check
  325. *
  326. * @return A string[] containing displayable components
  327. * 0 - mode
  328. * 1 - nickname
  329. * 2 - ident
  330. * 3 - hostname
  331. */
  332. private String[] getDetails(final GroupChatUser client) {
  333. if (client == null) {
  334. // WTF?
  335. throw new UnsupportedOperationException("getDetails called with"
  336. + " null ChannelClientInfo");
  337. }
  338. final String[] res = {
  339. getModes(client),
  340. Styliser.CODE_NICKNAME + client.getNickname() + Styliser.CODE_NICKNAME,
  341. client.getUsername().orElse(""),
  342. client.getHostname().orElse(""),};
  343. if (showColours) {
  344. final Optional<Colour> foreground
  345. = client.getDisplayProperty(DisplayProperty.FOREGROUND_COLOUR);
  346. final Optional<Colour> background
  347. = client.getDisplayProperty(DisplayProperty.BACKGROUND_COLOUR);
  348. if (foreground.isPresent()) {
  349. String prefix = Styliser.CODE_HEXCOLOUR + ColourUtils.getHex(foreground.get());
  350. if (background.isPresent()) {
  351. prefix += ',' + ColourUtils.getHex(background.get());
  352. }
  353. res[1] = prefix + res[1] + Styliser.CODE_HEXCOLOUR;
  354. }
  355. }
  356. return res;
  357. }
  358. @Override
  359. protected boolean processNotificationArg(final Object arg, final List<Object> args) {
  360. if (arg instanceof User) {
  361. final User clientInfo = (User) arg;
  362. args.add(clientInfo.getNickname());
  363. args.add(clientInfo.getUsername());
  364. args.add(clientInfo.getHostname());
  365. return true;
  366. } else if (arg instanceof GroupChatUser) {
  367. final GroupChatUser clientInfo = (GroupChatUser) arg;
  368. args.addAll(Arrays.asList(getDetails(clientInfo)));
  369. return true;
  370. } else if (arg instanceof Topic) {
  371. // Format topics
  372. final Topic topic = (Topic) arg;
  373. args.add("");
  374. args.add(topic.getClient().map(GroupChatUser::getNickname).orElse("Unknown"));
  375. args.add(topic.getClient().flatMap(GroupChatUser::getUsername).orElse(""));
  376. args.add(topic.getClient().flatMap(GroupChatUser::getHostname).orElse(""));
  377. args.add(topic.getTopic());
  378. args.add(topic.getDate().getTime());
  379. return true;
  380. } else {
  381. // Everything else - default formatting
  382. return super.processNotificationArg(arg, args);
  383. }
  384. }
  385. @Override
  386. protected void modifyNotificationArgs(final List<Object> actionArgs,
  387. final List<Object> messageArgs) {
  388. messageArgs.add(channelInfo.getName());
  389. }
  390. // ---------------------------------------------------- TOPIC HANDLING -----
  391. /**
  392. * Adds the specified topic to this channel's topic list.
  393. *
  394. * @param topic The topic to be added.
  395. */
  396. public void addTopic(final Topic topic) {
  397. synchronized (topics) {
  398. topics.add(topic);
  399. }
  400. updateTitle();
  401. }
  402. @Override
  403. public List<Topic> getTopics() {
  404. synchronized (topics) {
  405. return new ArrayList<>(topics);
  406. }
  407. }
  408. @Override
  409. public Optional<Topic> getCurrentTopic() {
  410. synchronized (topics) {
  411. if (topics.isEmpty()) {
  412. return Optional.empty();
  413. } else {
  414. return Optional.of(getTopics().get(topics.size() - 1));
  415. }
  416. }
  417. }
  418. // ------------------------------------------ PARSER METHOD DELEGATION -----
  419. @Override
  420. public void setTopic(final String topic) {
  421. channelInfo.setTopic(topic);
  422. }
  423. @Override
  424. public int getMaxTopicLength() {
  425. return connection.getParser().get().getMaxTopicLength();
  426. }
  427. @Override
  428. public Optional<Connection> getConnection() {
  429. return Optional.of(connection);
  430. }
  431. @Override
  432. public Optional<GroupChatUser> getUser(final User user) {
  433. final ChannelClientInfo ci = channelInfo.getChannelClient(((Client) user).getClientInfo());
  434. if (ci == null) {
  435. return Optional.empty();
  436. }
  437. return Optional.of(groupChatUserManager.getUserFromClient(ci, user, this));
  438. }
  439. @Override
  440. public Collection<GroupChatUser> getUsers() {
  441. return channelInfo.getChannelClients().stream()
  442. .map(client -> groupChatUserManager.getUserFromClient(client, this))
  443. .collect(Collectors.toList());
  444. }
  445. @Override
  446. public WindowModel getWindowModel() {
  447. return this;
  448. }
  449. @Override
  450. public void kick(final GroupChatUser user, final Optional<String> reason) {
  451. ((ChannelClient) user).getClientInfo().kick(
  452. reason.orElse(getConfigManager().getOption("general", "kickmessage")));
  453. }
  454. @Override
  455. public Collection<ChannelListModeItem> getListModeItems(final char mode) {
  456. return channelInfo.getListMode(mode);
  457. }
  458. @Override
  459. public void setMode(final char mode, @Nullable final String value) {
  460. channelInfo.alterMode(true, mode, value);
  461. }
  462. @Override
  463. public void removeMode(final char mode, final String value) {
  464. channelInfo.alterMode(false, mode, value);
  465. }
  466. @Override
  467. public void flushModes() {
  468. channelInfo.flushModes();
  469. }
  470. @Override
  471. public String getModes() {
  472. return channelInfo.getModes();
  473. }
  474. @Override
  475. public String getModeValue(final char mode) {
  476. return channelInfo.getMode(mode);
  477. }
  478. @Override
  479. public void requestUsersInfo() {
  480. channelInfo.sendWho();
  481. }
  482. }