From 1758011f24444a409284d051e874ea22e077f466 Mon Sep 17 00:00:00 2001 From: Greg Holmes Date: Sun, 18 Jan 2015 22:59:53 +0000 Subject: [PATCH] Some more work on the channel who plugin. --- .../addons/channelwho/ChannelWhoModule.java | 12 ++ .../addons/channelwho/ConnectionHandler.java | 87 +++++++- .../channelwho/ConnectionHandlerFactory.java | 20 +- .../channelwho/ConnectionHandlerTest.java | 192 ++++++++++++++++++ 4 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 channelwho/test/com/dmdirc/addons/channelwho/ConnectionHandlerTest.java diff --git a/channelwho/src/com/dmdirc/addons/channelwho/ChannelWhoModule.java b/channelwho/src/com/dmdirc/addons/channelwho/ChannelWhoModule.java index 0693eb2fd..aba29712f 100644 --- a/channelwho/src/com/dmdirc/addons/channelwho/ChannelWhoModule.java +++ b/channelwho/src/com/dmdirc/addons/channelwho/ChannelWhoModule.java @@ -23,7 +23,13 @@ package com.dmdirc.addons.channelwho; import com.dmdirc.ClientModule; +import com.dmdirc.DMDircMBassador; import com.dmdirc.plugins.PluginDomain; +import com.dmdirc.util.LoggingScheduledExecutorService; + +import java.util.concurrent.ScheduledExecutorService; + +import javax.inject.Named; import dagger.Module; import dagger.Provides; @@ -44,4 +50,10 @@ public ChannelWhoModule(final String domain) { public String getSettingsDomain() { return domain; } + + @Provides + @Named("channelwho") + public ScheduledExecutorService getExecutorService(final DMDircMBassador eventBus) { + return new LoggingScheduledExecutorService(1, eventBus, "channelwho"); + } } diff --git a/channelwho/src/com/dmdirc/addons/channelwho/ConnectionHandler.java b/channelwho/src/com/dmdirc/addons/channelwho/ConnectionHandler.java index 38b76e454..d6ba0f146 100644 --- a/channelwho/src/com/dmdirc/addons/channelwho/ConnectionHandler.java +++ b/channelwho/src/com/dmdirc/addons/channelwho/ConnectionHandler.java @@ -22,8 +22,27 @@ package com.dmdirc.addons.channelwho; +import com.dmdirc.config.ConfigBinder; +import com.dmdirc.config.ConfigBinding; +import com.dmdirc.events.ChannelUserAwayEvent; +import com.dmdirc.events.DisplayProperty; +import com.dmdirc.events.ServerNumericEvent; import com.dmdirc.interfaces.Connection; +import com.dmdirc.interfaces.ConnectionManager; import com.dmdirc.interfaces.GroupChat; +import com.dmdirc.interfaces.GroupChatUser; +import com.dmdirc.interfaces.config.AggregateConfigProvider; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import net.engio.mbassy.listener.Handler; /** * Responsible for managing timers and settings required to who any {@link GroupChat}s on a @@ -31,19 +50,85 @@ */ public class ConnectionHandler { + private final Map users; private final Connection connection; private final String domain; + private final ScheduledExecutorService executorService; + private final ConnectionManager connectionManager; + private final ConfigBinder configBinder; + private int whoInterval; + private ScheduledFuture future; - public ConnectionHandler(final Connection connection, final String domain) { + public ConnectionHandler( + final AggregateConfigProvider config, + final ScheduledExecutorService executorService, + final ConnectionManager connectionManager, final String domain, + final Connection connection) { this.connection = connection; this.domain = domain; + this.executorService = executorService; + this.connectionManager = connectionManager; + configBinder = config.getBinder().withDefaultDomain(domain); + users = new HashMap<>(); } public void load() { + configBinder.bind(this, ConnectionHandler.class); connection.getWindowModel().getEventBus().subscribe(this); } public void unload() { + configBinder.unbind(this); + executorService.shutdown(); connection.getWindowModel().getEventBus().unsubscribe(this); + if (future != null) { + future.cancel(true); + } + } + + @VisibleForTesting + void checkWho() { + connectionManager.getConnections().forEach(connection -> + connection.getGroupChatManager().getChannels().forEach(channel -> { + if (channel.getWindowModel().getConfigManager().getOptionBool(domain, "sendWho")) { + channel.requestUsersInfo(); + } + })); + } + + @VisibleForTesting + @ConfigBinding(key="whoInterval") + void handleWhoInterval(final int value) { + whoInterval = value; + if (future != null) { + future.cancel(true); + } + future = executorService.schedule(this::checkWho, whoInterval, TimeUnit.MILLISECONDS); + } + + @VisibleForTesting + @Handler + void handleAwayEvent(final ChannelUserAwayEvent event) { + if (!event.getReason().isPresent()) { + event.setDisplayProperty(DisplayProperty.DO_NOT_DISPLAY, true); + users.put(event.getUser().getNickname(), event.getUser()); + event.getChannel().getConnection() + .ifPresent(c -> c.requestUserInfo(event.getUser().getUser())); + } + } + + @VisibleForTesting + @Handler + void handleServerNumericEvent(final ServerNumericEvent event) { + if (event.getNumeric() == 301) { + final String nickname = event.getArgs()[4]; + final String reason = event.getArgs()[5]; + final GroupChatUser user = users.remove(nickname); + if (user != null) { + connection.getWindowModel().getEventBus().publishAsync( + new ChannelUserAwayEvent(user.getGroupChat(), user, + Optional.ofNullable(reason))); + } + } } } diff --git a/channelwho/src/com/dmdirc/addons/channelwho/ConnectionHandlerFactory.java b/channelwho/src/com/dmdirc/addons/channelwho/ConnectionHandlerFactory.java index 18fa475c0..53166300a 100644 --- a/channelwho/src/com/dmdirc/addons/channelwho/ConnectionHandlerFactory.java +++ b/channelwho/src/com/dmdirc/addons/channelwho/ConnectionHandlerFactory.java @@ -22,25 +22,41 @@ package com.dmdirc.addons.channelwho; +import com.dmdirc.ClientModule.GlobalConfig; import com.dmdirc.interfaces.Connection; +import com.dmdirc.interfaces.ConnectionManager; +import com.dmdirc.interfaces.config.AggregateConfigProvider; import com.dmdirc.plugins.PluginDomain; +import java.util.concurrent.ScheduledExecutorService; + import javax.inject.Inject; +import javax.inject.Named; /** * Factory for creating {@link ConnectionHandler}s. */ public class ConnectionHandlerFactory { + private final AggregateConfigProvider config; + private final ScheduledExecutorService executorService; + private final ConnectionManager connectionManager; private final String domain; @Inject - public ConnectionHandlerFactory(@PluginDomain(ChannelWhoPlugin.class) final String domain) { + public ConnectionHandlerFactory(@GlobalConfig final AggregateConfigProvider config, + @Named("channelwho") final ScheduledExecutorService executorService, + final ConnectionManager connectionManager, + @PluginDomain(ChannelWhoPlugin.class) final String domain) { + this.config = config; + this.executorService = executorService; + this.connectionManager = connectionManager; this.domain = domain; } public ConnectionHandler get(final Connection connection) { - final ConnectionHandler handler = new ConnectionHandler(connection, domain); + final ConnectionHandler handler = new ConnectionHandler(config, executorService, + connectionManager, domain, connection); handler.load(); return handler; } diff --git a/channelwho/test/com/dmdirc/addons/channelwho/ConnectionHandlerTest.java b/channelwho/test/com/dmdirc/addons/channelwho/ConnectionHandlerTest.java new file mode 100644 index 000000000..79d6a53d1 --- /dev/null +++ b/channelwho/test/com/dmdirc/addons/channelwho/ConnectionHandlerTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2006-2015 DMDirc Developers + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.dmdirc.addons.channelwho; + +import com.dmdirc.DMDircMBassador; +import com.dmdirc.config.ConfigBinder; +import com.dmdirc.events.ChannelUserAwayEvent; +import com.dmdirc.events.DisplayProperty; +import com.dmdirc.events.ServerNumericEvent; +import com.dmdirc.interfaces.Connection; +import com.dmdirc.interfaces.ConnectionManager; +import com.dmdirc.interfaces.GroupChat; +import com.dmdirc.interfaces.GroupChatManager; +import com.dmdirc.interfaces.GroupChatUser; +import com.dmdirc.interfaces.User; +import com.dmdirc.interfaces.WindowModel; +import com.dmdirc.interfaces.config.AggregateConfigProvider; + +import com.google.common.collect.Lists; + +import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ConnectionHandlerTest { + + @Mock private AggregateConfigProvider config; + @Mock private ConfigBinder configBinder; + @Mock private WindowModel windowModel; + @Mock private DMDircMBassador eventBus; + @Mock private ScheduledExecutorService scheduledExecutorService; + @Mock private ScheduledFuture scheduledFuture; + @Mock private ConnectionManager connectionManager; + @Mock private Connection connection; + @Mock private GroupChat groupChat; + @Mock private GroupChatUser groupChatUser; + @Mock private User user; + @Mock private GroupChatManager groupChatManager; + @Mock private ServerNumericEvent serverNumericEvent; + @Mock private ChannelUserAwayEvent channelUserAwayEvent; + @Captor private ArgumentCaptor eventArgumentCaptor; + private ConnectionHandler instance; + + @Before + public void setUp() throws Exception { + when(scheduledExecutorService.schedule(any(Runnable.class), anyLong(), any())) + .thenReturn(scheduledFuture); + when(windowModel.getEventBus()).thenReturn(eventBus); + when(connection.getWindowModel()).thenReturn(windowModel); + when(config.getBinder()).thenReturn(configBinder); + when(connectionManager.getConnections()).thenReturn(Lists.newArrayList(connection)); + when(connection.getGroupChatManager()).thenReturn(groupChatManager); + when(groupChatManager.getChannels()).thenReturn(Lists.newArrayList(groupChat)); + when(groupChat.getWindowModel()).thenReturn(windowModel); + when(groupChat.getConnection()).thenReturn(Optional.of(connection)); + when(configBinder.withDefaultDomain("domain")).thenReturn(configBinder); + when(windowModel.getConfigManager()).thenReturn(config); + when(groupChatUser.getNickname()).thenReturn("nickname"); + when(channelUserAwayEvent.getUser()).thenReturn(groupChatUser); + when(channelUserAwayEvent.getChannel()).thenReturn(groupChat); + when(groupChatUser.getUser()).thenReturn(user); + instance = new ConnectionHandler(config, scheduledExecutorService, connectionManager, + "domain", connection); + instance.handleWhoInterval(5); + } + + @Test + public void testLoad() throws Exception { + instance.load(); + verify(configBinder).bind(instance, ConnectionHandler.class); + verify(eventBus).subscribe(instance); + verify(scheduledExecutorService).schedule(any(Runnable.class), eq(5l), + eq(TimeUnit.MILLISECONDS)); + } + + @Test + public void testUnload() throws Exception { + instance.unload(); + verify(configBinder).unbind(instance); + verify(scheduledExecutorService).shutdown(); + verify(eventBus).unsubscribe(instance); + } + + @Test + public void testHandleWhoInterval() throws Exception { + instance.handleWhoInterval(10); + verify(scheduledFuture).cancel(true); + verify(scheduledExecutorService).schedule(any(Runnable.class), eq(5l), + eq(TimeUnit.MILLISECONDS)); + verify(scheduledExecutorService).schedule(any(Runnable.class), eq(10l), + eq(TimeUnit.MILLISECONDS)); + } + + @Test + public void testCheckWho_True() throws Exception { + when(config.getOptionBool("domain", "sendWho")).thenReturn(true); + instance.checkWho(); + verify(config).getOptionBool("domain", "sendWho"); + verify(groupChat).requestUsersInfo(); + } + + @Test + public void testCheckWho_False() throws Exception { + when(config.getOptionBool("domain", "sendWho")).thenReturn(false); + instance.checkWho(); + verify(config).getOptionBool("domain", "sendWho"); + verify(groupChat, never()).requestUsersInfo(); + } + + @Test + public void testHandleAwayEvent_WithReason() throws Exception { + when(channelUserAwayEvent.getReason()).thenReturn(Optional.ofNullable("reason")); + instance.load(); + instance.handleAwayEvent(channelUserAwayEvent); + verify(channelUserAwayEvent, never()) + .setDisplayProperty(DisplayProperty.DO_NOT_DISPLAY, true); + verify(connection, never()).requestUserInfo(any()); + } + + @Test + public void testHandleAwayEvent_WithoutReason() throws Exception { + when(channelUserAwayEvent.getReason()).thenReturn(Optional.empty()); + instance.load(); + instance.handleAwayEvent(channelUserAwayEvent); + verify(channelUserAwayEvent).setDisplayProperty(DisplayProperty.DO_NOT_DISPLAY, true); + verify(connection).requestUserInfo(any()); + } + + @Test + public void testHandleServerNumericEvent_301() throws Exception { + when(serverNumericEvent.getNumeric()).thenReturn(301); + when(serverNumericEvent.getArgs()).thenReturn( + new String[]{"", "", "", "", "nickname", "reason"}); + instance.load(); + when(channelUserAwayEvent.getReason()).thenReturn(Optional.empty()); + when(groupChatUser.getGroupChat()).thenReturn(groupChat); + instance.handleAwayEvent(channelUserAwayEvent); + instance.handleServerNumericEvent(serverNumericEvent); + verify(eventBus).publishAsync(eventArgumentCaptor.capture()); + assertEquals("nickname", eventArgumentCaptor.getValue().getUser().getNickname()); + assertEquals("reason", eventArgumentCaptor.getValue().getReason().get()); + } + + @Test + public void testHandleServerNumericEvent_101() throws Exception { + when(serverNumericEvent.getNumeric()).thenReturn(101); + when(channelUserAwayEvent.getReason()).thenReturn(Optional.empty()); + instance.load(); + instance.handleAwayEvent(channelUserAwayEvent); + instance.handleServerNumericEvent(serverNumericEvent); + verify(serverNumericEvent, never()).getArgs(); + verify(eventBus, never()).publishAsync(any()); + } +} \ No newline at end of file