2018-04-14 19:32:48 +08:00
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System ;
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
using System.Linq ;
using osu.Framework.Allocation ;
using osu.Framework.Configuration ;
using osu.Framework.Graphics ;
using osu.Framework.Logging ;
using osu.Framework.Threading ;
using osu.Game.Online.API ;
using osu.Game.Online.API.Requests ;
using osu.Game.Users ;
namespace osu.Game.Online.Chat
{
/// <summary>
/// Manages everything channel related
/// </summary>
public class ChannelManager : Component , IOnlineComponent
{
/// <summary>
/// The channels the player joins on startup
/// </summary>
private readonly string [ ] defaultChannels =
{
@"#lazer" ,
@"#osu" ,
@"#lobby"
} ;
/// <summary>
/// The currently opened channel
/// </summary>
public Bindable < Channel > CurrentChannel { get ; } = new Bindable < Channel > ( ) ;
/// <summary>
/// The Channels the player has joined
/// </summary>
public ObservableCollection < Channel > JoinedChannels { get ; } = new ObservableCollection < Channel > ( ) ;
/// <summary>
/// The channels available for the player to join
/// </summary>
public ObservableCollection < Channel > AvailableChannels { get ; } = new ObservableCollection < Channel > ( ) ;
2018-07-24 10:54:11 +08:00
private readonly IncomingMessagesHandler channelMessagesHandler ;
private readonly IncomingMessagesHandler privateMessagesHandler ;
2018-04-19 02:46:42 +08:00
private IAPIProvider api ;
2018-04-14 19:32:48 +08:00
private ScheduledDelegate fetchMessagesScheduleder ;
2018-07-24 10:54:11 +08:00
/// <summary>
/// Opens a channel or switches to the channel if already opened.
/// </summary>
/// <exception cref="ChannelNotFoundException">If the name of the specifed channel was not found this exception will be thrown.</exception>
/// <param name="name"></param>
2018-04-14 19:32:48 +08:00
public void OpenChannel ( string name )
{
if ( name = = null )
throw new ArgumentNullException ( nameof ( name ) ) ;
CurrentChannel . Value = AvailableChannels . FirstOrDefault ( c = > c . Name = = name )
2018-07-10 00:45:11 +08:00
? ? throw new ChannelNotFoundException ( name ) ;
2018-04-14 19:32:48 +08:00
}
2018-07-24 10:54:11 +08:00
/// <summary>
/// Opens a new private channel.
/// </summary>
2018-07-24 11:14:33 +08:00
/// <param name="user">The user the private channel is opened with.</param>
2018-07-24 10:54:11 +08:00
public void OpenPrivateChannel ( User user )
2018-04-14 19:32:48 +08:00
{
if ( user = = null )
throw new ArgumentNullException ( nameof ( user ) ) ;
CurrentChannel . Value = JoinedChannels . FirstOrDefault ( c = > c . Target = = TargetType . User & & c . Id = = user . Id )
2018-07-24 02:46:44 +08:00
? ? new PrivateChannel { User = user } ;
2018-04-14 19:32:48 +08:00
}
public ChannelManager ( )
{
CurrentChannel . ValueChanged + = currentChannelChanged ;
2018-07-24 10:54:11 +08:00
2018-07-24 21:19:50 +08:00
channelMessagesHandler = new IncomingMessagesHandler (
2018-07-25 00:01:28 +08:00
lastId = > new GetMessagesRequest ( JoinedChannels . Where ( c = > c . Target = = TargetType . Channel ) , lastId ) , handleChannelMessages ) ;
2018-07-24 10:54:11 +08:00
2018-07-24 21:19:50 +08:00
privateMessagesHandler = new IncomingMessagesHandler (
2018-07-25 00:01:28 +08:00
lastId = > new GetPrivateMessagesRequest ( lastId ) , handleUserMessages ) ;
2018-04-14 19:32:48 +08:00
}
private void currentChannelChanged ( Channel channel )
{
if ( ! JoinedChannels . Contains ( channel ) )
JoinedChannels . Add ( channel ) ;
}
/// <summary>
/// Posts a message to the currently opened channel.
/// </summary>
/// <param name="text">The message text that is going to be posted</param>
/// <param name="isAction">Is true if the message is an action, e.g.: user is currently eating </param>
public void PostMessage ( string text , bool isAction = false )
{
if ( CurrentChannel . Value = = null )
return ;
2018-07-10 03:00:39 +08:00
var currentChannel = CurrentChannel . Value ;
2018-04-14 19:32:48 +08:00
if ( ! api . IsLoggedIn )
{
2018-07-10 03:00:39 +08:00
currentChannel . AddNewMessages ( new ErrorMessage ( "Please sign in to participate in chat!" ) ) ;
2018-04-14 19:32:48 +08:00
return ;
}
var message = new LocalEchoMessage
{
Sender = api . LocalUser . Value ,
Timestamp = DateTimeOffset . Now ,
TargetType = CurrentChannel . Value . Target ,
TargetId = CurrentChannel . Value . Id ,
IsAction = isAction ,
Content = text
} ;
2018-07-10 03:00:39 +08:00
currentChannel . AddLocalEcho ( message ) ;
2018-04-14 19:32:48 +08:00
var req = new PostMessageRequest ( message ) ;
2018-07-10 02:39:16 +08:00
req . Failure + = exception = >
{
Logger . Error ( exception , "Posting message failed." ) ;
2018-07-10 03:00:39 +08:00
currentChannel . ReplaceMessage ( message , null ) ;
2018-07-10 02:39:16 +08:00
} ;
2018-07-10 03:00:39 +08:00
req . Success + = m = > currentChannel . ReplaceMessage ( message , m ) ;
2018-04-14 19:32:48 +08:00
api . Queue ( req ) ;
}
2018-07-24 11:14:33 +08:00
/// <summary>
/// Posts a command locally. Commands like /help will result in a help message written in the current channel.
/// </summary>
/// <param name="text">the text containing the command identifier and command parameters.</param>
2018-04-14 19:32:48 +08:00
public void PostCommand ( string text )
{
if ( CurrentChannel . Value = = null )
return ;
var parameters = text . Split ( new [ ] { ' ' } , 2 ) ;
string command = parameters [ 0 ] ;
string content = parameters . Length = = 2 ? parameters [ 1 ] : string . Empty ;
switch ( command )
{
case "me" :
if ( string . IsNullOrWhiteSpace ( content ) )
{
CurrentChannel . Value . AddNewMessages ( new ErrorMessage ( "Usage: /me [action]" ) ) ;
break ;
}
PostMessage ( content , true ) ;
break ;
case "help" :
CurrentChannel . Value . AddNewMessages ( new InfoMessage ( "Supported commands: /help, /me [action]" ) ) ;
break ;
default :
CurrentChannel . Value . AddNewMessages ( new ErrorMessage ( $@"""/{command}"" is not supported! For a list of supported commands see /help" ) ) ;
break ;
}
}
private void fetchNewMessages ( )
{
2018-07-24 10:54:11 +08:00
if ( channelMessagesHandler . CanRequestNewMessages )
channelMessagesHandler . RequestNewMessages ( api ) ;
2018-04-14 19:32:48 +08:00
2018-07-24 10:54:11 +08:00
if ( privateMessagesHandler . CanRequestNewMessages )
privateMessagesHandler . RequestNewMessages ( api ) ;
2018-04-14 19:32:48 +08:00
}
private void handleUserMessages ( IEnumerable < Message > messages )
{
2018-07-24 11:17:57 +08:00
var joinedPrivateChannels = JoinedChannels . Where ( c = > c . Target = = TargetType . User ) . ToList ( ) ;
2018-04-14 19:32:48 +08:00
2018-07-24 03:15:52 +08:00
Channel getChannelForUser ( User user )
2018-04-14 19:32:48 +08:00
{
2018-07-24 11:17:57 +08:00
var channel = joinedPrivateChannels . FirstOrDefault ( c = > c . Id = = user . Id ) ;
2018-04-14 19:32:48 +08:00
if ( channel = = null )
{
2018-07-24 03:15:52 +08:00
channel = new PrivateChannel { User = user } ;
2018-04-14 19:32:48 +08:00
JoinedChannels . Add ( channel ) ;
2018-07-24 11:17:57 +08:00
joinedPrivateChannels . Add ( channel ) ;
2018-04-14 19:32:48 +08:00
}
2018-07-24 03:15:52 +08:00
return channel ;
}
long localUserId = api . LocalUser . Value . Id ;
var outgoingGroups = messages . Where ( m = > m . Sender . Id = = localUserId ) . GroupBy ( m = > m . TargetId ) ;
var incomingGroups = messages . Where ( m = > m . Sender . Id ! = localUserId ) . GroupBy ( m = > m . UserId ) ;
foreach ( var group in incomingGroups )
{
var targetUser = group . First ( ) . Sender ;
var channel = getChannelForUser ( targetUser ) ;
channel . AddNewMessages ( group . ToArray ( ) ) ;
var outgoingTargetMessages = outgoingGroups . FirstOrDefault ( g = > g . Key = = targetUser . Id ) ;
2018-04-14 19:32:48 +08:00
if ( outgoingTargetMessages ! = null )
channel . AddNewMessages ( outgoingTargetMessages . ToArray ( ) ) ;
}
2018-07-24 03:15:52 +08:00
// Because of the way the API provides data right now, outgoing messages do not contain required
// user (or in the future, target channel) metadata. As such we need to do a second request
// to find out the specifics of the user.
2018-07-24 11:17:57 +08:00
var withoutReplyGroups = outgoingGroups . Where ( g = > joinedPrivateChannels . All ( m = > m . Id ! = g . Key ) ) ;
2018-04-14 19:32:48 +08:00
foreach ( var withoutReplyGroup in withoutReplyGroups )
{
var userReq = new GetUserRequest ( withoutReplyGroup . First ( ) . TargetId ) ;
userReq . Failure + = exception = > Logger . Error ( exception , "Failed to get user informations." ) ;
userReq . Success + = user = >
{
2018-07-24 03:15:52 +08:00
var channel = getChannelForUser ( user ) ;
2018-04-14 19:32:48 +08:00
channel . AddNewMessages ( withoutReplyGroup . ToArray ( ) ) ;
} ;
api . Queue ( userReq ) ;
}
}
private void handleChannelMessages ( IEnumerable < Message > messages )
{
var channels = JoinedChannels . ToList ( ) ;
foreach ( var group in messages . GroupBy ( m = > m . TargetId ) )
channels . Find ( c = > c . Id = = group . Key ) ? . AddNewMessages ( group . ToArray ( ) ) ;
}
private void initializeDefaultChannels ( )
{
var req = new ListChannelsRequest ( ) ;
req . Success + = channels = >
{
2018-07-24 23:51:20 +08:00
foreach ( var channel in channels )
{
if ( JoinedChannels . Any ( c = > c . Id = = channel . Id ) )
continue ;
// add as available if not already
if ( AvailableChannels . All ( c = > c . Id ! = channel . Id ) )
AvailableChannels . Add ( channel ) ;
// join any channels classified as "defaults"
if ( defaultChannels . Any ( c = > c . Equals ( channel . Name , StringComparison . OrdinalIgnoreCase ) ) )
{
JoinedChannels . Add ( channel ) ;
2018-07-30 03:40:43 +08:00
FetchInitalMessages ( channel ) ;
2018-07-24 23:51:20 +08:00
}
}
2018-04-14 19:32:48 +08:00
fetchNewMessages ( ) ;
} ;
2018-07-24 10:54:11 +08:00
req . Failure + = error = >
{
Logger . Error ( error , "Fetching channel list failed" ) ;
initializeDefaultChannels ( ) ;
} ;
2018-04-14 19:32:48 +08:00
api . Queue ( req ) ;
}
2018-07-30 03:40:43 +08:00
/// <summary>
/// Fetches inital messages of a channel
///
/// TODO: remove this when the API supports returning initial fetch messages for more than one channel by specifying the last message id per channel instead of one last message id globally.
/// right now it caps out at 50 messages and therefore only returns one channel's worth of content.
/// </summary>
/// <param name="channel">The channel </param>
public void FetchInitalMessages ( Channel channel )
{
var fetchInitialMsgReq = new GetMessagesRequest ( new [ ] { channel } , null ) ;
fetchInitialMsgReq . Success + = handleChannelMessages ;
fetchInitialMsgReq . Failure + = exception = > Logger . Error ( exception , $"Failed to fetch inital messages for the channel {channel.Name}" ) ;
api . Queue ( fetchInitialMsgReq ) ;
}
2018-04-14 19:32:48 +08:00
public void APIStateChanged ( APIAccess api , APIState state )
{
switch ( state )
{
case APIState . Online :
if ( JoinedChannels . Count = = 0 )
initializeDefaultChannels ( ) ;
2018-07-24 10:54:11 +08:00
2018-04-19 02:46:42 +08:00
fetchMessagesScheduleder = Scheduler . AddDelayed ( fetchNewMessages , 1000 , true ) ;
2018-04-14 19:32:48 +08:00
break ;
default :
2018-07-24 10:54:11 +08:00
channelMessagesHandler . CancelOngoingRequests ( ) ;
privateMessagesHandler . CancelOngoingRequests ( ) ;
2018-04-14 19:32:48 +08:00
fetchMessagesScheduleder ? . Cancel ( ) ;
2018-07-24 10:54:11 +08:00
fetchMessagesScheduleder = null ;
2018-04-14 19:32:48 +08:00
break ;
}
}
[BackgroundDependencyLoader]
private void load ( IAPIProvider api )
{
2018-04-19 02:46:42 +08:00
this . api = api ;
2018-04-14 19:32:48 +08:00
api . Register ( this ) ;
}
}
2018-07-10 00:45:11 +08:00
2018-07-24 11:14:33 +08:00
/// <summary>
/// An exception thrown when a channel could not been found.
/// </summary>
2018-07-10 00:45:11 +08:00
public class ChannelNotFoundException : Exception
{
public ChannelNotFoundException ( string channelName )
: base ( $"A channel with the name {channelName} could not be found." )
{
}
}
2018-04-14 19:32:48 +08:00
}