Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
631 lines (559 sloc) 21.9 KB
// Smuxi - Smart MUltipleXed Irc
//
// Copyright (c) 2012-2014 Mirco Bauer <meebey@meebey.net>
//
// Full GPL License: <http://www.gnu.org/licenses/gpl.txt>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Collections.Generic;
using JabbR.Client;
using JabbR.Client.Models;
using Microsoft.AspNet.SignalR.Client.Transports;
using Microsoft.AspNet.SignalR.Client.Http;
using Smuxi.Common;
namespace Smuxi.Engine
{
// https://github.com/davidfowl/Jabbot/blob/master/Jabbot/Bot.cs
// https://github.com/davidfowl/JabbR/blob/master/JabbR/Hubs/Chat.cs
[ProtocolManagerInfo(Name = "JabbR", Description = "JabbR Chat", Alias = "jabbr")]
public class JabbrProtocolManager : ProtocolManagerBase
{
#if LOG4NET
private static readonly log4net.ILog Logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endif
const string LibraryTextDomain = "smuxi-engine-jabbr";
ChatModel ProtocolChat { get; set; }
JabbRClient Client { get; set; }
string Username { get; set; }
ServerModel Server { get; set; }
public override string NetworkID {
get {
if (Server == null) {
return Protocol;
}
return Server.Hostname;
}
}
public override string Protocol {
get {
return "JabbR";
}
}
public override ChatModel Chat {
get {
return ProtocolChat;
}
}
public JabbrProtocolManager(Session session) : base(session)
{
Trace.Call(session);
}
public override bool Command(CommandModel cmd)
{
Trace.Call(cmd);
if (cmd.IsCommand) {
var handled = false;
switch (cmd.Command) {
case "help":
CommandHelp(cmd);
handled = true;
break;
case "j":
case "join":
CommandJoin(cmd);
handled = true;
break;
}
return handled;
} else {
CommandMessage(cmd);
}
return true;
}
public void CommandHelp(CommandModel cmd)
{
Trace.Call(cmd);
// TRANSLATOR: this line is used as a label / category for a
// list of commands below
var builder = CreateMessageBuilder().
AppendEventPrefix().
AppendHeader(_("JabbR Commands"));
Session.AddMessageToFrontend(cmd, builder.ToMessage());
string[] help = {
"connect jabbr username password",
"join"
};
foreach (string line in help) {
builder = CreateMessageBuilder();
builder.AppendEventPrefix();
builder.AppendText(line);
Session.AddMessageToFrontend(cmd, builder.ToMessage());
}
}
public void CommandJoin(CommandModel cmd)
{
Trace.Call(cmd);
if (String.IsNullOrEmpty(cmd.Parameter)) {
NotEnoughParameters(cmd);
return;
}
try {
Client.JoinRoom(cmd.Parameter);
} catch (Exception ex) {
#if LOG4NET
Logger.Error(ex);
#endif
var msg = CreateMessageBuilder().
AppendEventPrefix().
AppendErrorText(_("Joining room failed. Reason: {0}"),
ex.Message).
ToMessage();
Session.AddMessageToFrontend(cmd, msg);
}
}
public void CommandMessage(CommandModel cmd)
{
Trace.Call(cmd);
try {
switch (cmd.Chat.ChatType) {
case ChatType.Group:
Client.Send(cmd.Data, cmd.Chat.ID).Wait();
break;
case ChatType.Person:
Client.SendPrivateMessage(cmd.Chat.ID, cmd.Data).Wait();
break;
}
} catch (Exception ex) {
#if LOG4NET
Logger.Error(ex);
#endif
var msg = CreateMessageBuilder().
AppendEventPrefix().
AppendErrorText(_("Sending message failed. Reason: {0}"),
ex.Message).
ToMessage();
Session.AddMessageToFrontend(cmd, msg);
}
}
public override void Connect(FrontendManager fm, ServerModel server)
{
Trace.Call(fm, server);
Server = server;
Username = server.Username;
var chatName = String.Format("{0} {1}", Protocol, NetworkID);
ProtocolChat = new ProtocolChatModel(NetworkID, chatName, this);
ProtocolChat.InitMessageBuffer(MessageBufferPersistencyType.Volatile);
Session.AddChat(Chat);
Session.SyncChat(Chat);
try {
string url;
if (server.Hostname.StartsWith("http://") ||
server.Hostname.StartsWith("https://")) {
url = server.Hostname;
} else {
if (server.UseEncryption && server.Port == 443) {
url = String.Format("https://{0}", server.Hostname);
} else if (server.UseEncryption) {
url = String.Format("https://{0}:{1}",
server.Hostname, server.Port);
} else if (!server.UseEncryption && server.Port == 80) {
url = String.Format("http://{0}", server.Hostname);
} else {
url = String.Format("http://{0}:{1}",
server.Hostname, server.Port);
}
}
// HACK: SignalR's ServerSentEventsTransport times out on Mono
// for some reason and then fallbacks to LongPollingTransport
// this takes 10 seconds though, so let's go LP directly
Func<IClientTransport> transport = null;
if (Type.GetType("Mono.Runtime") == null) {
transport = () => new AutoTransport(new DefaultHttpClient());
} else {
transport = () => new LongPollingTransport();
}
var authProvider = new DefaultAuthenticationProvider(url);
Client = new JabbRClient(url, authProvider, transport);
Client.AutoReconnect = true;
Client.MessageReceived += OnMessageReceived;
Client.MeMessageReceived += OnMeMessageReceived;
Client.UserLeft += OnUserLeft;
Client.UserJoined += OnUserJoined;
Client.JoinedRoom += OnJoinedRoom;
Client.PrivateMessage += OnPrivateMessage;
Me = CreatePerson(Username);
Me.IdentityNameColored.ForegroundColor = new TextColor(0, 0, 255);
Me.IdentityNameColored.BackgroundColor = TextColor.None;
Me.IdentityNameColored.Bold = true;
Connect();
} catch (Exception ex) {
#if LOG4NET
Logger.Error(ex);
#endif
var msg = CreateMessageBuilder().
AppendEventPrefix().
AppendErrorText(_("Connection failed! Reason: {0}"),
ex.Message).
ToMessage();
Session.AddMessageToChat(ProtocolChat, msg);
}
}
void Connect()
{
Trace.Call();
var msg = CreateMessageBuilder().
AppendEventPrefix().
AppendText(_("Connecting to {0}..."), Client.SourceUrl).
ToMessage();
Session.AddMessageToChat(ProtocolChat, msg);
if (!Server.ValidateServerCertificate) {
var whitelist = Session.CertificateValidator.HostnameWhitelist;
lock (whitelist) {
var hostname = Server.Hostname;
if (hostname.StartsWith("http://") ||
hostname.StartsWith("https://")) {
hostname = new Uri(hostname).Host;
}
if (!whitelist.Contains(hostname)) {
whitelist.Add(hostname);
}
}
}
var res = Client.Connect(Server.Username, Server.Password);
res.Wait();
// HACK: this event can only be subscribed if we have made an
// actual connection o_O
Client.Disconnected += OnDisconnected;
IsConnected = true;
OnConnected(EventArgs.Empty);
OnLoggedOn(res.Result.Rooms);
}
void OnPrivateMessage(string fromUserName, string toUserName, string message)
{
Trace.Call(fromUserName, toUserName, message);
string targetChat;
string targetUser;
if (fromUserName == Username) {
targetChat = toUserName;
targetUser = toUserName;
} else {
targetChat = fromUserName;
targetUser = fromUserName;
}
var chat = (PersonChatModel) GetChat(targetChat, ChatType.Person);
if (chat == null) {
var person = CreatePerson(targetUser);
chat = new PersonChatModel(person, targetUser, targetUser, this);
chat.InitMessageBuffer(MessageBufferPersistencyType.Volatile);
Session.AddChat(chat);
Session.SyncChat(chat);
}
var builder = CreateMessageBuilder<JabbrMessageBuilder>();
if (fromUserName == Username) {
builder.AppendSenderPrefix(Me);
} else {
builder.AppendSenderPrefix(chat.Person, true);
}
builder.AppendMessage(message);
Session.AddMessageToChat(chat, builder.ToMessage());
}
void OnDisconnected()
{
Trace.Call();
foreach (var chat in Chats) {
// don't disable the protocol chat, else the user loses all
// control for the protocol manager! e.g. after a manual
// reconnect or server-side disconnect
if (chat.ChatType == ChatType.Protocol) {
continue;
}
Session.DisableChat(chat);
}
IsConnected = false;
OnDisconnected(EventArgs.Empty);
}
void OnJoinedRoom(Room room)
{
Trace.Call(room);
var groupChat = new GroupChatModel(room.Name, room.Name, this);
groupChat.InitMessageBuffer(MessageBufferPersistencyType.Volatile);
var task = Client.GetRoomInfo(room.Name);
task.Wait();
// check task.Exception
var roomInfo = task.Result;
groupChat.Topic = CreateMessageBuilder<JabbrMessageBuilder>().
AppendMessage(roomInfo.Topic).
ToMessage();
foreach (var user in roomInfo.Users) {
groupChat.UnsafePersons.Add(user.Name,
CreatePerson(user));
}
// add ourself if needed
if (!groupChat.UnsafePersons.ContainsKey(Username)) {
groupChat.UnsafePersons.Add(Username, Me);
}
Session.AddChat(groupChat);
Session.SyncChat(groupChat);
}
public override void Reconnect(FrontendManager fm)
{
Trace.Call(fm);
var msg = CreateMessageBuilder().
AppendEventPrefix().
AppendText(_("Reconnecting to {0}..."), Server.Hostname).
ToMessage();
Session.AddMessageToChat(Chat, msg);
try {
Client.Disconnect();
Connect();
} catch (Exception ex) {
#if LOG4NET
Logger.Error("Reconnect(): Exception during reconnect", ex);
#endif
}
}
public override void Disconnect(FrontendManager fm)
{
Trace.Call(fm);
Client.Disconnect();
}
public override IList<GroupChatModel> FindGroupChats(GroupChatModel filter)
{
Trace.Call(filter);
var res = Client.GetRooms();
res.Wait();
// res.Exception
var groupChats = new List<GroupChatModel>();
foreach (var room in res.Result) {
var groupChat = new GroupChatModel(room.Name, room.Name, this);
groupChat.InitMessageBuffer(MessageBufferPersistencyType.Volatile);
groupChat.PersonCount = room.Count;
groupChats.Add(groupChat);
}
return groupChats;
}
public override void OpenChat(FrontendManager fm, ChatModel chat)
{
Trace.Call(fm, chat);
switch (chat.ChatType) {
case ChatType.Person:
var personChat = (PersonChatModel) GetChat(chat.ID, ChatType.Person);
if (personChat != null) {
return;
}
var person = CreatePerson(chat.ID);
personChat = new PersonChatModel(person, chat.ID, chat.ID, this);
personChat.InitMessageBuffer(MessageBufferPersistencyType.Volatile);
Session.AddChat(personChat);
Session.SyncChat(personChat);
break;
case ChatType.Group:
Client.JoinRoom(chat.ID);
break;
}
}
public override void CloseChat(FrontendManager fm, ChatModel chatInfo)
{
Trace.Call(fm, chatInfo);
// get real chat object from session
var chat = GetChat(chatInfo.ID, chatInfo.ChatType);
if (chat == null) {
#if LOG4NET
Logger.Error("CloseChat(): Session.GetChat(" +
chatInfo.ID + ", " + chatInfo.ChatType + ")" +
" returned null!");
#endif
return;
}
switch (chat.ChatType) {
case ChatType.Person:
Session.RemoveChat(chat);
break;
case ChatType.Group:
Client.LeaveRoom(chat.ID);
break;
}
}
public override void SetPresenceStatus(PresenceStatus status, string message)
{
//throw new NotImplementedException();
}
public override string ToString()
{
string result = Chat.Name;
if (!IsConnected) {
result += " (" + _("not connected") + ")";
}
return result;
}
void OnMessageReceived(Message message, string room)
{
Trace.Call(message, room);
var chat = GetChat(room, ChatType.Group) ?? ProtocolChat;
AddMessage(chat, message);
}
void AddMessage(ChatModel chat, Message msg)
{
if (chat == null) {
throw new ArgumentNullException("chat");
}
if (msg == null) {
throw new ArgumentNullException("msg");
}
string content = msg.Content;
string name = msg.User.Name;
var builder = CreateMessageBuilder<JabbrMessageBuilder>();
if (msg.When != default(DateTimeOffset)) {
builder.TimeStamp = msg.When.UtcDateTime;
}
var sender = name == Username ? Me : CreatePerson(name);
builder.AppendSenderPrefix(sender);
builder.AppendMessage(content);
if (sender != Me) {
builder.MarkHighlights();
}
var message = builder.ToMessage();
Session.AddMessageToChat(chat, message);
if (sender == Me) {
// server echos our sent messages for us
OnMessageSent(
new MessageEventArgs(chat, message, null, chat.ID)
);
} else {
OnMessageReceived(
new MessageEventArgs(chat, message, name, chat.ID)
);
}
}
void OnMeMessageReceived(string userName, string content, string roomName)
{
Trace.Call(userName, content, roomName);
var chat = GetChat(roomName, ChatType.Group) ?? ProtocolChat;
var builder = CreateMessageBuilder<JabbrMessageBuilder>().
AppendActionPrefix().
AppendIdendityName(GetPerson<PersonModel>(chat, userName)).
AppendSpace().
AppendMessage(content);
if (userName != Username) {
builder.MarkHighlights();
}
var msg = builder.ToMessage();
Session.AddMessageToChat(chat, msg);
OnMessageReceived(
new MessageEventArgs(chat, msg, userName, roomName)
);
}
void OnUserJoined(User user, string room, bool isOwner)
{
Trace.Call(user, room, isOwner);
var chat = (GroupChatModel) GetChat(room, ChatType.Group);
if (chat == null) {
return;
}
var person = CreatePerson(user.Name);
lock (chat) {
if (chat.Persons.ContainsKey(person.ID)) {
#if LOG4NET
Logger.Warn("OnUserJoined(): person already on chat, ignoring...");
#endif
return;
}
Session.AddPersonToGroupChat(chat, person);
}
}
void OnUserLeft(User user, string room)
{
Trace.Call(user, room);
var chat = (GroupChatModel) GetChat(room, ChatType.Group);
if (chat == null) {
return;
}
if (user.Name == Username) {
Session.RemoveChat(chat);
return;
}
PersonModel person = null;
if (chat.Persons.TryGetValue(user.Name, out person)) {
Session.RemovePersonFromGroupChat(chat, person);
}
}
void OnLoggedOn(IEnumerable<Room> rooms)
{
Trace.Call(rooms);
try {
foreach (var room in rooms) {
var groupChat = (GroupChatModel) GetChat(room.Name, ChatType.Group);
bool newChat;
if (groupChat == null) {
groupChat = new GroupChatModel(room.Name, room.Name, this);
groupChat.InitMessageBuffer(MessageBufferPersistencyType.Volatile);
newChat = true;
} else {
groupChat.UnsafePersons.Clear();
newChat = false;
}
var task = Client.GetRoomInfo(room.Name);
task.Wait();
// check task.Exception
var roomInfo = task.Result;
groupChat.Topic = CreateMessageBuilder<JabbrMessageBuilder>().
AppendMessage(roomInfo.Topic).
ToMessage();
foreach (var user in roomInfo.Users) {
groupChat.UnsafePersons.Add(user.Name,
CreatePerson(user));
}
// add ourself if needed
if (!groupChat.UnsafePersons.ContainsKey(Username)) {
groupChat.UnsafePersons.Add(Username, Me);
}
foreach (var msg in roomInfo.RecentMessages) {
AddMessage(groupChat, msg);
}
if (newChat) {
Session.AddChat(groupChat);
} else {
Session.EnableChat(groupChat);
}
Session.SyncChat(groupChat);
}
} catch (Exception ex) {
#if LOG4NET
Logger.Error(ex);
#endif
var msg = CreateMessageBuilder().
AppendEventPrefix().
AppendErrorText(_("Retrieving chat information failed. " +
"Reason: {0}"),
ex.Message).
ToMessage();
Session.AddMessageToChat(ProtocolChat, msg);
}
}
PersonModel CreatePerson(User user)
{
return CreatePerson(user.Name);
}
PersonModel CreatePerson(string username)
{
return new PersonModel(username, username, NetworkID, Protocol, this);
}
static string _(string msg)
{
return LibraryCatalog.GetString(msg, LibraryTextDomain);
}
}
}