using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security.AccessControl;
using System.Text.RegularExpressions;
using System.Threading;
using Misuzilla.Applications.TwitterIrcGateway.Filter;
using Misuzilla.Net.Irc;
using System.Security.Permissions;
using System.Security;
namespace Misuzilla.Applications.TwitterIrcGateway
{
///
/// ユーザからの接続を管理するクラス
///
public partial class Session : SessionBase, IDisposable
{
private readonly static String ConfigBasePath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Configs");
private Server _server;
private TwitterService _twitter;
private LinkedList _lastStatusIdsFromGateway;
private Dictionary _lastStatusIdsByScreenName;
private Groups _groups;
private Filters _filter;
private Config _config;
private AddInManager _addinManager;
private HashSet _followingUsers = new HashSet();
private Boolean _isFirstTime = true;
private User _twitterUser;
#region Events
///
/// IRCメッセージ受信時、TwitterIrcGatewayが処理する前のイベント
///
public event EventHandler PreMessageReceived;
///
/// IRCメッセージ受信時のイベント
///
public event EventHandler MessageReceived;
///
/// IRCメッセージ受信時、TwitterIrcGatewayが処理した後のイベント
///
public event EventHandler PostMessageReceived;
///
/// セッション開始時のイベント
///
public event EventHandler SessionStarted;
///
/// セッション終了時のイベント
///
public event EventHandler SessionEnded;
///
/// 設定変更時のイベント
///
public event EventHandler ConfigChanged;
///
/// アドインをすべて読み込んで Initialize 完了時のイベント
///
public event EventHandler AddInsLoadCompleted;
///
/// 受信したタイムラインステータスのセットを処理前のイベント
///
public event EventHandler PreProcessTimelineStatuses;
///
/// タイムラインステータスを処理前のイベント
///
public event EventHandler PreProcessTimelineStatus;
///
/// フィルタ処理前のイベント
///
public event EventHandler PreFilterProcessTimelineStatus;
///
/// フィルタ処理後のイベント
///
public event EventHandler PostFilterProcessTimelineStatus;
///
/// タイムラインステータスの送信前のイベント
///
public event EventHandler PreSendMessageTimelineStatus;
///
/// 受信したステータスメッセージの送信先グループ決定時のイベント
///
public event EventHandler MessageRoutedTimelineStatus;
///
/// 受信したステータスメッセージをグループに送信前のイベント
///
public event EventHandler PreSendGroupMessageTimelineStatus;
///
/// 受信したステータスメッセージをグループに送信後のイベント
///
public event EventHandler PostSendGroupMessageTimelineStatus;
///
/// タイムラインステータスをすべてのグループに送信後のイベント
///
public event EventHandler PostSendMessageTimelineStatus;
///
/// タイムラインステータスを処理後のイベント
///
public event EventHandler PostProcessTimelineStatus;
///
/// 受信したタイムラインステータスのセットを処理後のイベント
///
public event EventHandler PostProcessTimelineStatuses;
///
/// IRCクライアントからのステータス更新要求を受信時のイベント
///
public event EventHandler UpdateStatusRequestReceived;
///
/// Twitterのステータス更新を行う直前時のイベント
///
public event EventHandler PreSendUpdateStatus;
///
/// Twitterのステータス更新を行った直後時のイベント
///
public event EventHandler PostSendUpdateStatus;
///
/// IRCクライアントからのステータス更新要求が完了時のイベント
///
public event EventHandler UpdateStatusRequestCommited;
#endregion
public Session(User user, Server server) : base(user.Id, server)
{
_Counter.Increment(ref _Counter.Session);
_twitterUser = user;
_groups = new Groups();
_filter = new Filters();
_config = new Config();
_server = server;
_lastStatusIdsFromGateway = new LinkedList();
_lastStatusIdsByScreenName = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
_addinManager = new AddInManager(_server, this);
//_addinManager = AddInManager.CreateInstanceWithAppDomain(_server, this);
Logger = new SessionTraceLogger(this);
PostWaitList = new List>();
}
~Session()
{
//_Counter.Decrement(ref _Counter.Session);
Dispose();
}
///
/// 開始されているかどうかを返します。
///
public Boolean IsStarted
{
get;
private set;
}
///
/// 現在のニックネームを取得します
///
public String Nick
{
get { return CurrentNick; }
}
///
/// セッションに結びつけられたTwitterへのAPIアクセスのためのサービスを取得します
///
public TwitterService TwitterService
{
get { return _twitter; }
}
///
/// セッションに結びつけられた設定を取得します
///
public Config Config
{
get { return _config; }
}
///
/// セッションが持つグループのコレクションを取得します
///
public Groups Groups
{
get { return _groups; }
}
///
/// セッションが持つフィルタのコレクションを取得します
///
public Filters Filters
{
get { return _filter; }
}
///
/// セッションのアドインマネージャを取得します
///
public AddInManager AddInManager
{
get { return _addinManager; }
}
///
/// ユーザの設定が保存されているディレクトリのパスを取得します
///
public String UserConfigDirectory
{
#if HOSTING
get { return Path.Combine(ConfigBasePath, _twitterUser.Id.ToString()); }
#else
get { return Path.Combine(ConfigBasePath, _twitterUser.ScreenName); }
#endif
}
///
/// 接続に利用しているTwitterのアカウントのユーザ情報を取得します
///
public User TwitterUser
{
get { return _twitterUser; }
}
///
/// ログを出力するためのクラスのインスタンスを取得します
///
public Logger Logger
{
get; private set;
}
///
/// フォローしているユーザの一覧(DisableUserListが有効の場合には空になります)
///
public HashSet FollowingUsers
{
get { return _followingUsers; }
}
///
/// 送信予定のステータスメッセージキューを取得します。
///
public List> PostWaitList
{
get; private set;
}
///
/// セッションを開始します。
///
private void Start()
{
CheckDisposed();
// アドインの読み込み
SendTwitterGatewayServerMessage("* アドインを読み込んでいます...");
_addinManager.Load();
FireEvent(AddInsLoadCompleted, EventArgs.Empty);
SendTwitterGatewayServerMessage("* アドインを読み込みました。");
_twitter.Start();
IsStarted = true;
}
#region イベント実行メソッド
internal virtual void OnAddInsLoadCompleted()
{
FireEvent(AddInsLoadCompleted, EventArgs.Empty);
}
protected virtual void OnMessageReceived(IRCMessage msg, MessageReceivedEventArgs e)
{
Debug.WriteLine(msg.ToString());
if (FireEvent(PreMessageReceived, e))
{
if (FireEvent(MessageReceived, e))
{
FireEvent(PostMessageReceived, e);
}
}
}
protected virtual void OnSessionStarted(String username)
{
FireEvent(SessionStarted, new SessionStartedEventArgs(username, _twitterUser, (IPEndPoint)Connections[0].TcpClient.Client.RemoteEndPoint));
}
protected virtual void OnSessionEnded()
{
FireEvent(SessionEnded, EventArgs.Empty);
}
internal virtual void OnConfigChanged()
{
if (ConfigChanged != null)
ConfigChanged(this, EventArgs.Empty);
#if FALSE
if (_traceListener == null && _config.EnableTrace)
{
_traceListener = new IrcTraceListener(this);
Trace.Listeners.Add(_traceListener);
}
else if ((_traceListener != null) && !_config.EnableTrace)
{
Trace.Listeners.Remove(_traceListener);
_traceListener = null;
}
#endif
}
public void LoadSettings()
{
LoadConfig();
OnConfigChanged();
LoadGroups();
LoadFilters();
}
public String GetSettingPath(String fileName)
{
return Path.Combine(UserConfigDirectory, fileName);
}
///
///
///
public void LoadFilters()
{
// filters 読み取り
String path = GetSettingPath("Filters.xml");
try
{
_filter = Filters.Load(path);
}
catch (IOException ie)
{
SendTwitterGatewayServerMessage("エラー: " + ie.Message);
}
}
///
///
///
public void SaveFilters()
{
lock (_filter)
{
String path = GetSettingPath("Filters.xml");
try
{
_filter.Save(path);
}
catch (IOException ie)
{
SendTwitterGatewayServerMessage("エラー: " + ie.Message);
}
}
}
///
///
///
public void LoadGroups()
{
// group 読み取り
lock (_groups)
{
String path = GetSettingPath("Groups.xml");
try
{
_groups = Groups.Load(path);
// 下位互換性FIX: グループに自分自身のNICKは存在しないようにします
foreach (Group g in _groups.Values)
{
g.Members.Remove(CurrentNick);
}
}
catch (IOException ie)
{
SendTwitterGatewayServerMessage("エラー: " + ie.Message);
}
}
}
///
///
///
public void SaveGroups()
{
// group 読み取り
lock (_groups)
{
String path = GetSettingPath("Groups.xml");
try
{
_groups.Save(path);
}
catch (IOException ie)
{
SendTwitterGatewayServerMessage("エラー: " + ie.Message);
}
}
}
///
///
///
public void LoadConfig()
{
lock (_config)
{
String path = GetSettingPath("Config.xml");
try
{
_config = Config.Load(path);
}
catch (IOException ie)
{
SendTwitterGatewayServerMessage("エラー: " + ie.Message);
}
}
}
///
///
///
public void SaveConfig()
{
lock (_config)
{
String path = GetSettingPath("Config.xml");
try
{
_config.Save(path);
OnConfigChanged();
}
catch (IOException ie)
{
SendTwitterGatewayServerMessage("エラー: " + ie.Message);
}
}
}
#endregion
private Group GetGroupByChannelName(String channelName)
{
// グループを取得/作成
Group group;
if (!_groups.TryGetValue(channelName, out group))
{
group = new Group(channelName);
_groups.Add(channelName, group);
}
return group;
}
private void InitializeSession()
{
// 設定を読み込む
SendTwitterGatewayServerMessage("* 設定を読み込んでいます...");
LoadSettings();
//
// Twitte Service Setup
//
_twitter = new TwitterService(Connections[0].UserInfo.UserName, Connections[0].UserInfo.Password);
_twitter.EnableCompression = Config.Default.EnableCompression; // TODO: なんとかする
_twitter.BufferSize = _config.BufferSize;
_twitter.Interval = _config.Interval;
_twitter.IntervalDirectMessage = _config.IntervalDirectMessage;
_twitter.IntervalReplies = _config.IntervalReplies;
_twitter.EnableRepliesCheck = _config.EnableRepliesCheck;
_twitter.POSTFetchMode = _config.POSTFetchMode;
_twitter.FetchCount = _config.FetchCount;
_twitter.FriendsPerPageThreshold = _config.FriendsPerPageThreshold;
_twitter.RepliesReceived += new EventHandler(twitter_RepliesReceived);
_twitter.TimelineStatusesReceived += new EventHandler(twitter_TimelineStatusesReceived);
_twitter.CheckError += new EventHandler(twitter_CheckError);
_twitter.DirectMessageReceived += new EventHandler(twitter_DirectMessageReceived);
if (_server.Proxy != null)
_twitter.Proxy = _server.Proxy;
// IRC メッセージ
MessageReceived += new EventHandler(MessageReceived_PRIVMSG);
MessageReceived += new EventHandler(MessageReceived_WHOIS);
MessageReceived += new EventHandler(MessageReceived_INVITE);
MessageReceived += new EventHandler(MessageReceived_JOIN);
MessageReceived += new EventHandler(MessageReceived_PART);
MessageReceived += new EventHandler(MessageReceived_KICK);
MessageReceived += new EventHandler(MessageReceived_LIST);
MessageReceived += new EventHandler(MessageReceived_TOPIC);
MessageReceived += new EventHandler(MessageReceived_MODE);
MessageReceived += new EventHandler(MessageReceived_PING);
#if ENABLE_IM_SUPPORT
MessageReceived += new EventHandler(MessageReceived_TIGIMENABLE);
MessageReceived += new EventHandler(MessageReceived_TIGIMDISABLE);
if (!String.IsNullOrEmpty(_config.IMServiceServerName))
{
ConnectToIMService(true);
}
#endif
SendTwitterGatewayServerMessage("* セッションを開始しました。");
OnSessionStarted(_twitterUser.ScreenName);
Logger.Information("SessionStarted: UserName={0}; Nickname={1}", Connections[0].UserInfo.UserName, CurrentNick);
Logger.Information("User: Id={0}, ScreenName={1}, Name={2}", _twitterUser.Id, _twitterUser.ScreenName, _twitterUser.Name);
}
#region メッセージ処理イベント
private void MessageReceived_JOIN(object sender, MessageReceivedEventArgs e)
{
if (!(e.Message is JoinMessage)) return;
if (String.IsNullOrEmpty(e.Message.CommandParams[0]))
{
SendErrorReply(ErrorReply.ERR_NEEDMOREPARAMS, "Not enough parameters");
return;
}
JoinMessage joinMsg = e.Message as JoinMessage;
Logger.Information(String.Format("Join: {0} -> {1}", joinMsg.Sender, joinMsg.Channel));
String[] channelNames = joinMsg.Channel.Split(new Char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (String channelName in channelNames)
{
// メインチャンネルはスキップ
if (String.Compare(channelName, _config.ChannelName, true) == 0)
continue;
if (!channelName.StartsWith("#") || channelName.Length < 3)
{
Debug.WriteLine(String.Format("No nick/such channel: {0}", channelName));
SendErrorReply(ErrorReply.ERR_NOSUCHCHANNEL, "No such nick/channel");
continue;
}
// グループを取得/作成
Group group = GetGroupByChannelName(channelName);
if (!group.IsJoined)
{
JoinChannel(this, group);
}
}
}
private void MessageReceived_PART(object sender, MessageReceivedEventArgs e)
{
if (!(e.Message is PartMessage)) return;
if (String.IsNullOrEmpty(e.Message.CommandParams[0]))
{
SendErrorReply(ErrorReply.ERR_NEEDMOREPARAMS, "Not enough parameters");
return;
}
PartMessage partMsg = e.Message as PartMessage;
Logger.Information("Part: {0} -> {1}", partMsg.Sender, partMsg.Channel);
String[] channelNames = partMsg.Channel.Split(new Char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (String channelName in channelNames)
{
// メインチャンネルはスキップ
if (String.Compare(channelName, _config.ChannelName, true) == 0)
continue;
if (!channelName.StartsWith("#") || channelName.Length < 3)
{
SendErrorReply(ErrorReply.ERR_NOSUCHCHANNEL, "No such nick/channel");
continue;
}
// グループを取得/作成
Group group;
if (_groups.TryGetValue(channelName, out group))
{
group.IsJoined = false;
}
else
{
SendErrorReply(ErrorReply.ERR_NOTONCHANNEL, "You're not on that channel");
continue;
}
partMsg = new PartMessage(channelName, "");
SendServer(partMsg);
// もう捨てていい?
if (group.Members.Count == 0)
{
_groups.Remove(group.Name);
SendTwitterGatewayServerMessage("グループ \""+group.Name+"\" を削除しました。");
}
}
SaveGroups();
}
private void MessageReceived_KICK(object sender, MessageReceivedEventArgs e)
{
if (String.Compare(e.Message.Command, "KICK", true) != 0) return;
String[] channels = e.Message.CommandParams[0].Split(new Char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
String[] kickTargets = e.Message.CommandParams[1].Split(new Char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (channels.Length == 0 || (channels.Length != 1 && channels.Length != kickTargets.Length))
{
SendErrorReply(ErrorReply.ERR_NEEDMOREPARAMS, "Not enough parameters");
return;
}
if (channels.Length == 1)
{
// 一チャンネルから複数けりだす
Group group;
if (!_groups.TryGetValue(channels[0], out group))
{
SendErrorReply(ErrorReply.ERR_NOTONCHANNEL, "You're not on that channel");
return;
}
foreach (String kickTarget in kickTargets)
{
if (group.Exists(kickTarget))
{
group.Remove(kickTarget);
OtherMessage kickMsg = new OtherMessage("KICK");
kickMsg.Sender = e.Message.Sender;
kickMsg.CommandParams[0] = channels[0];
kickMsg.CommandParams[1] = kickTarget;
kickMsg.CommandParams[2] = e.Message.CommandParams[2];
Send(kickMsg);
}
else
{
SendErrorReply(ErrorReply.ERR_NOSUCHNICK, "No such nick/channel");
return;
}
}
}
else
{
// 複数チャンネルからそれぞれ
for (Int32 i = 0; i < channels.Length; i++)
{
String channelName = channels[i];
Group group;
if (!_groups.TryGetValue(channelName, out group))
{
SendErrorReply(ErrorReply.ERR_NOTONCHANNEL, "You're not on that channel");
return;
}
if (group.Exists(kickTargets[i]))
{
group.Remove(kickTargets[i]);
OtherMessage kickMsg = new OtherMessage("KICK");
kickMsg.Sender = e.Message.Sender;
kickMsg.CommandParams[0] = group.Name;
kickMsg.CommandParams[1] = kickTargets[i];
kickMsg.CommandParams[2] = e.Message.CommandParams[2];
Send(kickMsg);
}
else
{
SendErrorReply(ErrorReply.ERR_NOSUCHNICK, "No such nick/channel");
return;
}
}
}
SaveGroups();
}
private void MessageReceived_LIST(object sender, MessageReceivedEventArgs e)
{
if (String.Compare(e.Message.Command, "LIST", true) != 0) return;
foreach (Group group in _groups.Values)
{
SendNumericReply(NumericReply.RPL_LIST, group.Name, group.Members.Count.ToString(), "");
}
SendNumericReply(NumericReply.RPL_LISTEND, "End of LIST");
}
private void MessageReceived_INVITE(object sender, MessageReceivedEventArgs e)
{
if (String.Compare(e.Message.Command, "INVITE", true) != 0) return;
if (String.IsNullOrEmpty(e.Message.CommandParams[0]))
{
SendErrorReply(ErrorReply.ERR_NONICKNAMEGIVEN, "No nickname given");
return;
}
String userName = e.Message.CommandParams[0];
String channelName = e.Message.CommandParams[1];
Logger.Information("Invite: {0} -> {1}", userName, channelName);
if (!channelName.StartsWith("#") || channelName.Length < 3 || String.Compare(channelName, _config.ChannelName, true) == 0)
{
SendErrorReply(ErrorReply.ERR_NOSUCHCHANNEL, "No such nick/channel");
return;
}
// グループを取得、ユーザ追加
Group group = GetGroupByChannelName(channelName);
if (!group.Exists(userName))
{
group.Add(userName);
}
if (group.IsJoined)
{
JoinMessage joinMsg = new JoinMessage(channelName, "");
joinMsg.SenderHost = "twitter@" + Server.ServerName;
joinMsg.SenderNick = userName;
Send(joinMsg);
}
SaveGroups();
}
void MessageReceived_PRIVMSG(object sender, MessageReceivedEventArgs e)
{
PrivMsgMessage message = e.Message as PrivMsgMessage;
if (message == null) return;
StatusUpdateEventArgs eventArgs = new StatusUpdateEventArgs(message, message.Content);
if (!FireEvent(UpdateStatusRequestReceived, eventArgs)) return;
UpdateStatusWithReceiverDeferred(message.Receiver, eventArgs.Text);
}
#region Update Status Methods
///
/// 設定された時間待機した後Twitterのステータスを更新し、失敗した場合には指定されたチャンネルに通知し、リトライします。
///
///
///
///
public Deferred.DeferredState UpdateStatusWithReceiverDeferred(String receiver, String message)
{
return UpdateStatusWithReceiverDeferred(receiver, message, 0);
}
///
/// 設定された時間待機した後Twitterのステータスを更新し、失敗した場合には指定されたチャンネルに通知し、リトライします。
///
///
///
///
///
public Deferred.DeferredState UpdateStatusWithReceiverDeferred(String receiver, String message, Int64 inReplyToId)
{
return UpdateStatusWithReceiverDeferred(receiver, message, inReplyToId, null);
}
///
/// 設定された時間待機した後Twitterのステータスを更新し、失敗した場合には指定されたチャンネルに通知し、リトライします。完了時に指定されたコールバックメソッドを呼び出します。
///
///
///
///
///
///
public Deferred.DeferredState UpdateStatusWithReceiverDeferred(String receiver, String message, Int64 inReplyToId, Action callback)
{
Deferred.DeferredState state = Deferred.DeferredInvoke, Boolean>(UpdateStatusWithReceiver, Config.UpdateDelayTime * 1000, (asyncResult) => {
Deferred.DeferredState state_ = asyncResult.AsyncState as Deferred.DeferredState;
// 送信リストから外す
lock (PostWaitList)
PostWaitList.Remove(state_);
}, receiver, message, inReplyToId, callback);
PostWaitList.Add(state);
return state;
}
///
/// Twitterのステータスを更新し、失敗した場合には指定されたチャンネルに通知し、リトライします。
///
///
///
///
public Boolean UpdateStatusWithReceiver(String receiver, String message)
{
return UpdateStatusWithReceiver(receiver, message, 0);
}
///
/// Twitterのステータスを更新し、失敗した場合には指定されたチャンネルに通知し、リトライします。
///
///
///
///
///
public Boolean UpdateStatusWithReceiver(String receiver, String message, Int64 inReplyToId)
{
return UpdateStatusWithReceiver(receiver, message, inReplyToId, null);
}
///
/// Twitterのステータスを更新し、失敗した場合には指定されたチャンネルに通知し、リトライします。完了時に指定されたコールバックメソッドを呼び出します。
///
///
///
///
///
///
public Boolean UpdateStatusWithReceiver(String receiver, String message, Int64 inReplyToId, Action callback)
{
Boolean isRetry = false;
Boolean succeed = true;
Retry:
try
{
// チャンネル宛は自分のメッセージを書き換え
if ((String.Compare(receiver, _config.ChannelName, true) == 0) || receiver.StartsWith("#"))
{
String postMessage = message;
// 140文字制限のチェック
if (message.Length > 140)
{
Int32 overCharCount = message.Length - 140;
SendChannelMessage(receiver, Server.ServerNick,
String.Format("140文字を超えたメッセージの送信は出来ません。{0}文字の超過です。(場所: {1}...)", overCharCount, message.Substring(140, Math.Min(5, overCharCount))), true, false, false, true);
return false;
}
try
{
// InReplyId が 0 じゃないときは指定されている扱い
Status status = (inReplyToId > 0) ? UpdateStatus(message, inReplyToId) : UpdateStatus(message);
message = status.Text;
if (callback != null)
{
try
{
callback(status);
}
catch (Exception e)
{
Logger.Error(e.ToString());
}
}
if (!FireEvent(UpdateStatusRequestCommited, new TimelineStatusEventArgs(status))) return false;
}
catch (TwitterServiceException tse)
{
SendTwitterGatewayServerMessage("エラー: メッセージは完了しましたが、レスポンスを正しく受信できませんでした。(" + tse.Message + ")");
}
// ほかのグループに送信する
SendChannelMessage(receiver, CurrentNick, postMessage, false, true, true, false);
}
else if (String.Compare(receiver, "trace", true) != 0)
{
// 人に対する場合はDirect Message
_twitter.SendDirectMessage(receiver, message);
}
if (isRetry)
{
SendChannelMessage(receiver, Server.ServerNick, "メッセージ送信のリトライに成功しました。", true, false, false, true);
}
}
catch (WebException ex)
{
String content = String.Format("メッセージ送信に失敗しました({0})" + (!isRetry ? "/リトライします。" : ""), ex.Message.Replace("\n", " "));
SendChannelMessage(receiver, Server.ServerNick, content, true, false, false, true);
// 一回だけリトライするよ
if (!isRetry)
{
isRetry = true;
goto Retry;
}
else
{
succeed = false;
}
}
return succeed;
}
///
/// 設定された時間待機した後Twitterのステータスを更新します。
///
///
///
public Deferred.DeferredState UpdateStatusAsync(String message)
{
return Deferred.DeferredInvoke(UpdateStatus, Config.UpdateDelayTime * 1000, message);
}
///
/// 設定された時間待機した後Twitterのステータスを更新します。
///
///
///
public Deferred.DeferredState UpdateStatusAsync(String message, Int64 inReplyToStatusId)
{
return Deferred.DeferredInvoke(UpdateStatus, Config.UpdateDelayTime * 1000, message, inReplyToStatusId);
}
///
/// Twitterのステータスを更新します。
///
///
///
public Status UpdateStatus(String message)
{
// 送信(もし以前のスタイルのReplyが有効の場合には対象のユーザの最後に受信したIDにくっつける)
if (_config.EnableOldStyleReply)
{
Match match = Regex.Match(message, "^@([A-Za-z0-9_]+)");
if (match.Success && _lastStatusIdsByScreenName.ContainsKey(match.Groups[1].Value))
return UpdateStatus(message, _lastStatusIdsByScreenName[match.Groups[1].Value]);
else
return UpdateStatus(message, 0);
}
else
{
return UpdateStatus(message, 0);
}
}
///
/// Twitterのステータスを更新します。
///
///
///
///
public Status UpdateStatus(String message, Int64 inReplyToStatusId)
{
StatusUpdateEventArgs eventArgs = new StatusUpdateEventArgs(message, inReplyToStatusId);
if (!FireEvent(PreSendUpdateStatus, eventArgs)) return null;
Status status = _twitter.UpdateStatus(eventArgs.Text, inReplyToStatusId);
if (status != null)
{
Logger.Information("Status Update: {0} (ID:{1}, CreatedAt:{2}; InReplyToStatusId:{3})", status.Text, status.Id.ToString(), status.CreatedAt.ToString(), inReplyToStatusId);
_lastStatusIdsFromGateway.AddLast(status.Id);
if (_lastStatusIdsFromGateway.Count > 100)
{
_lastStatusIdsFromGateway.RemoveFirst();
}
}
eventArgs.CreatedStatus = status;
if (!FireEvent(PostSendUpdateStatus, eventArgs)) return null;
return status;
}
///
/// 遅延アップデートのキャンセルを試みます。
///
/// キャンセルに成功した場合にはtrue、キャンセルする対象が存在しなかった場合にはfalse
public Boolean TryCancelDeferredUpdate()
{
// まず送信待ちをみる
Deferred.DeferredState state = null;
lock (PostWaitList)
{
if (PostWaitList.Count > 0)
{
state = PostWaitList[0];
}
}
// 完了コールバック中でPostWaitListを触っているので外側でキャンセルする
if (state != null && state.Cancel())
{
// キャンセル出来た
return true;
}
return false;
}
#endregion
void MessageReceived_WHOIS(object sender, MessageReceivedEventArgs e)
{
if (String.Compare(e.Message.Command, "WHOIS", true) != 0) return;
// nick check
if (String.IsNullOrEmpty(e.Message.CommandParams[0]))
{
SendErrorReply(ErrorReply.ERR_NONICKNAMEGIVEN, "No nickname given");
return;
}
User user = null;
try
{
user = _twitter.GetUser(e.Message.CommandParams[0]);
if (user == null)
{
SendErrorReply(ErrorReply.ERR_NOSUCHNICK, "No such nick/channel");
}
}
catch (WebException we)
{
SendTwitterGatewayServerMessage("エラー: " + we.Message);
}
catch (TwitterServiceException tse)
{
SendTwitterGatewayServerMessage("エラー: " + tse.Message);
}
if (user == null)
return;
// ステータスをWHOIS replyとして返す
SendNumericReply(NumericReply.RPL_WHOISUSER, user.ScreenName, user.Id.ToString(), "localhost", "*", user.Name + " - " + user.Description);
SendNumericReply(NumericReply.RPL_WHOISSERVER, user.ScreenName, "WebSite", user.Url);
if (user.Status != null)
{
SendNumericReply(NumericReply.RPL_AWAY, user.ScreenName, user.Status.Text.Replace('\n', ' '));
SendNumericReply(NumericReply.RPL_WHOISIDLE
, user.ScreenName
, ((TimeSpan)(DateTime.Now - user.Status.CreatedAt)).TotalSeconds.ToString()
, "seconds idle");
}
SendNumericReply(NumericReply.RPL_ENDOFWHOIS, "End of /WHOIS list");
}
void MessageReceived_TOPIC(object sender, MessageReceivedEventArgs e)
{
if (String.Compare(e.Message.Command, "TOPIC", true) != 0) return;
TopicMessage topicMsg = e.Message as TopicMessage;
// client -> server (TOPIC #Channel :Topic Msg) && channel name != server primary channel(ex.#Twitter)
if (!String.IsNullOrEmpty(topicMsg.Topic) && (String.Compare(topicMsg.Channel, _config.ChannelName, true) != 0))
{
// Set channel topic
Group group = GetGroupByChannelName(topicMsg.Channel);
group.Topic = topicMsg.Topic;
SaveGroups();
// server -> client (set client topic)
Send(new TopicMessage(topicMsg.Channel, topicMsg.Topic){
SenderNick = CurrentNick
});
}
}
void MessageReceived_MODE(object sender, MessageReceivedEventArgs e)
{
if (String.Compare(e.Message.Command, "MODE", true) != 0) return;
ModeMessage modeMsg = e.Message as ModeMessage;
// チャンネルターゲットかつタイムラインチャンネル以外のみ
if (modeMsg.Target.StartsWith("#") && (String.Compare(modeMsg.Target, _config.ChannelName, true) != 0))
{
String channel = modeMsg.Target;
String modeArgs = modeMsg.ModeArgs;
Group group;
if (!_groups.TryGetValue(channel, out group))
{
SendErrorReply(ErrorReply.ERR_NOSUCHCHANNEL, "No such channel");
return;
}
foreach (ChannelMode mode in ChannelMode.Parse(modeArgs))
{
foreach (ChannelMode mode2 in new List(group.ChannelModes))
{
if (mode2.Mode == mode.Mode && mode2.Parameter == mode.Parameter)
{
if (mode.IsRemove)
{
// すでにあって削除
group.ChannelModes.Remove(mode2);
}
else
{
// すでにある
goto NEXT;
}
}
}
if (!mode.IsRemove)
{
group.ChannelModes.Add(mode);
}
SendServer(new ModeMessage(channel, mode.ToString()));
SaveGroups();
NEXT:
;
}
}
}
void MessageReceived_PING(object sender, MessageReceivedEventArgs e)
{
if (String.Compare(e.Message.Command, "PING", true) != 0) return;
Send(IRCMessage.CreateMessage("PONG :" + e.Message.CommandParam));
}
#endregion
#region Twitter Service イベント
void twitter_CheckError(object sender, ErrorEventArgs e)
{
SendServerErrorMessage(e.Exception.Message);
}
void twitter_DirectMessageReceived(object sender, DirectMessageEventArgs e)
{
// 初回は無視する
if (e.IsFirstTime)
return;
DirectMessage message = e.DirectMessage;
String text = (_config.ResolveTinyUrl) ? Utility.ResolveTinyUrlInMessage(message.Text) : message.Text;
String[] lines = text.Split(new Char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (String line in lines)
{
PrivMsgMessage privMsg = new PrivMsgMessage();
privMsg.SenderNick = message.SenderScreenName;
privMsg.SenderHost = "twitter@" + Server.ServerName;
privMsg.Receiver = CurrentNick;
//privMsg.Content = String.Format("{0}: {1}", screenName, text);
privMsg.Content = line;
Send(privMsg);
}
}
void twitter_TimelineStatusesReceived(object sender, StatusesUpdatedEventArgs e)
{
SendPing();
TimelineStatusesEventArgs eventArgs = new TimelineStatusesEventArgs(e.Statuses, _isFirstTime);
if (!FireEvent(PreProcessTimelineStatuses, eventArgs)) return;
// 初回だけは先にチェックしておかないとnamesが後から来てジャマ
if (_isFirstTime && !_config.DisableUserList)
{
CheckFriends();
}
Boolean friendsCheckRequired = e.FriendsCheckRequired;
foreach (Status status in e.Statuses.Status)
{
ProcessTimelineStatus(status, ref friendsCheckRequired);
}
// Friendsをチェックするのは成功して、チェックが必要となったとき
if (e.FriendsCheckRequired && !_config.DisableUserList)
{
CheckFriends();
}
if (!FireEvent(PostProcessTimelineStatuses, eventArgs)) return;
_isFirstTime = false;
}
void twitter_RepliesReceived(object sender, StatusesUpdatedEventArgs e)
{
Boolean dummy = false;
foreach (Status status in e.Statuses.Status)
{
ProcessTimelineStatus(status, ref dummy, false, e.IsFirstTime);
}
}
#endregion
#region Compatiblity
///
/// TwitterIrcGatewayからのメッセージを送信します
///
///
public void SendTwitterGatewayServerMessage(String message)
{
SendGatewayServerMessage(message);
}
#endregion
#region チャンネルメッセージ
///
/// ユーザ自身の更新メッセージなどをチャンネルに送信、必要なチャンネルにエコーバックします。
///
/// 送信するメッセージ
public void SendChannelMessage(String content)
{
SendChannelMessage(String.Empty, Connections[0].UserInfo.ClientHost, content, true, true, true, false);
}
///
/// ユーザ自身の更新メッセージなどをチャンネルに送信、必要なチャンネルにエコーバックします。
///
/// 入力のあったチャンネル
/// 送信するメッセージ
public void SendChannelMessage(String receivedChannel, String content)
{
SendChannelMessage(receivedChannel, Connections[0].UserInfo.ClientHost, content, true, true, true, false);
}
///
/// ユーザ自身の更新メッセージなどをチャンネルに送信、必要なチャンネルにエコーバックします。
///
/// 入力のあったチャンネル
/// 送信元
/// 送信するメッセージ
public void SendChannelMessage(String receivedChannel, String sender, String content)
{
SendChannelMessage(receivedChannel, sender, content, true, true, true, false);
}
///
/// メッセージをクライアントに送信、必要の応じてトピックに設定し、必要なチャンネルにエコーバックします。
///
/// 入力があったチャンネル
/// 送信するメッセージ
/// 送信元
/// 対象のチャンネルに送信するかどうかを指定します
/// ほかのチャンネルにエコーバックするかどうかを指定します
/// トピックに設定するかどうかを指定します
/// 送信メッセージを設定にかかわらずNOTICEにするかどうかを指定します
public void SendChannelMessage(String receivedChannel, String sender, String content, Boolean sendToTargetChannel, Boolean withEchoBack, Boolean setTopic, Boolean forceNotice)
{
// 改行は削除しておく
content = content.Replace("\n", "").Replace("\r", "");
// topicに設定する
if (_config.SetTopicOnStatusChanged && setTopic)
{
TopicMessage topicMsg = new TopicMessage(_config.ChannelName, content);
topicMsg.Sender = sender;
Send(topicMsg);
}
// 指定されたチャンネルに流す必要があればまず流す
if (sendToTargetChannel && !String.IsNullOrEmpty(receivedChannel))
{
if (_config.BroadcastUpdateMessageIsNotice || forceNotice)
{
Send(new NoticeMessage()
{
Sender = sender,
Receiver = receivedChannel,
Content = content
});
}
else
{
Send(new PrivMsgMessage()
{
Sender = sender,
Receiver = receivedChannel,
Content = content
});
}
}
// 他のチャンネルにも投げる
if (_config.BroadcastUpdate && withEchoBack)
{
// #Twitter
if (String.Compare(receivedChannel, _config.ChannelName, true) != 0)
{
// XXX: 例によってIRCライブラリのバージョンアップでどうにかしたい
if (_config.BroadcastUpdateMessageIsNotice || forceNotice)
{
Send(new NoticeMessage()
{
Sender = sender,
Receiver = _config.ChannelName,
Content = content
});
}
else
{
Send(new PrivMsgMessage()
{
Sender = sender,
Receiver = _config.ChannelName,
Content = content
});
}
}
// group
foreach (Group group in _groups.Values)
{
if (group.IsJoined && !group.IsSpecial && !group.IgnoreEchoBack && String.Compare(receivedChannel, group.Name, true) != 0)
{
if (_config.BroadcastUpdateMessageIsNotice || forceNotice)
{
Send(new NoticeMessage()
{
Sender = sender,
Receiver = group.Name,
Content = content
});
}
else
{
Send(new PrivMsgMessage()
{
Sender = sender,
Receiver = group.Name,
Content = content
});
}
}
}
}
// 全チャンネルエコーバックが有効でなく、チャンネルが指定されていない場合にはデフォルトに流す
else if (String.IsNullOrEmpty(receivedChannel))
{
if (forceNotice)
{
Send(new NoticeMessage()
{
Sender = sender,
Receiver = _config.ChannelName,
Content = content
});
}
else
{
Send(new PrivMsgMessage()
{
Sender = sender,
Receiver = _config.ChannelName,
Content = content
});
}
}
}
#endregion
///
///
///
///
///
public void JoinChannel(IIrcMessageSendable messageSendable, Group group)
{
JoinMessage joinMsg = new JoinMessage(group.Name, "");
messageSendable.SendServer(joinMsg);
messageSendable.SendNumericReply(NumericReply.RPL_NAMREPLY, "=", group.Name, String.Format("@{0} ", CurrentNick) + String.Join(" ", group.Members.ToArray()));
messageSendable.SendNumericReply(NumericReply.RPL_ENDOFNAMES, group.Name, "End of NAMES list");
group.IsJoined = true;
// mode
foreach (ChannelMode mode in group.ChannelModes)
{
messageSendable.Send(new ModeMessage(group.Name, mode.ToString()));
}
// Set topic of client, if topic was set
if (!String.IsNullOrEmpty(group.Topic))
{
messageSendable.Send(new TopicMessage(group.Name, group.Topic));
}
else
{
messageSendable.SendNumericReply(NumericReply.RPL_NOTOPIC, group.Name, "No topic is set");
}
}
///
///
///
private void GetFriendNames()
{
RunCheck(delegate
{
User[] friends = _twitter.GetFriends(10);
_followingUsers = new HashSet();
// 保持していてもしょうがないので消す
foreach (var friend in friends)
friend.Status = null;
_followingUsers.UnionWith(friends);
ShowChannelUsers(this);
});
}
///
/// チャンネルにユーザリストを送信します。
///
///
private void ShowChannelUsers(IIrcMessageSendable messageSendable)
{
List users = _followingUsers.Select(u => u.ScreenName).ToList();
for (var i = 0; i < ((users.Count / 100) + 1); i++)
{
Int32 count = Math.Min(users.Count - (i * 100), 100);
messageSendable.SendNumericReply(NumericReply.RPL_NAMREPLY, "=", _config.ChannelName, String.Format("{0} ", CurrentNick) + String.Join(" ", users.GetRange(i * 100, count).ToArray()));
}
messageSendable.SendNumericReply(NumericReply.RPL_ENDOFNAMES, _config.ChannelName, "End of NAMES list");
}
///
///
///
private void SendPing()
{
Send(new OtherMessage(String.Format("PING :{0}", Server.ServerName)));
}
///
///
///
private void CheckFriends()
{
if (_followingUsers.Count == 0)
{
GetFriendNames();
return;
}
RunCheck(delegate
{
User[] friends = _twitter.GetFriends(10);
// てきとうに。
// 増えた分
foreach (User user in friends)
{
if (!_followingUsers.Contains(user))
{
JoinMessage joinMsg = new JoinMessage(_config.ChannelName, "");
joinMsg.SenderNick = user.ScreenName;
joinMsg.SenderHost = String.Format("{0}@{1}", "twitter", Server.ServerName);
Send(joinMsg);
}
}
// 減った分
foreach (User user in _followingUsers)
{
if (!friends.Contains(user))
{
PartMessage partMsg = new PartMessage(_config.ChannelName, "");
partMsg.SenderNick = user.ScreenName;
partMsg.SenderHost = String.Format("{0}@{1}", "twitter", Server.ServerName);
Send(partMsg);
}
}
_followingUsers.IntersectWith(friends);
});
}
///
/// タイムラインのステータスを処理して、クライアントに送信します
///
///
///
public void ProcessTimelineStatus (Status status, ref Boolean friendsCheckRequired)
{
ProcessTimelineStatus(status, ref friendsCheckRequired, false);
}
public void ProcessTimelineStatus(Status status, ref Boolean friendsCheckRequired, Boolean ignoreGatewayCheck)
{
ProcessTimelineStatus(status, ref friendsCheckRequired, ignoreGatewayCheck, _isFirstTime);
}
public void ProcessTimelineStatus(Status status, ref Boolean friendsCheckRequired, Boolean ignoreGatewayCheck, Boolean isFirstTime)
{
TimelineStatusEventArgs eventArgs = new TimelineStatusEventArgs(status, status.Text, "PRIVMSG");
if (!FireEvent(PreProcessTimelineStatus, eventArgs)) return;
// チェック
// 自分がゲートウェイを通して発言したものは捨てる
if (!ignoreGatewayCheck && (status.User == null || String.IsNullOrEmpty(status.User.ScreenName) || _lastStatusIdsFromGateway.Contains(status.Id)))
{
return;
}
// @だけのReplyにIDをつけるモードがオンの時はStatusのIDを記録する
if (_config.EnableOldStyleReply)
{
_lastStatusIdsByScreenName[status.User.ScreenName] = status.Id;
}
// friends チェックが必要かどうかを確かめる
// まだないときは取ってくるフラグを立てる
friendsCheckRequired |= !(_followingUsers.Contains(status.User));
// フィルタ
if (!FireEvent(PreFilterProcessTimelineStatus, eventArgs)) return;
FilterArgs filterArgs = new FilterArgs(this, eventArgs.Text, status.User, eventArgs.IRCMessageType, false, status);
if (!_filter.ExecuteFilters(filterArgs))
{
// 捨てる
return;
}
eventArgs.IRCMessageType = filterArgs.IRCMessageType;
eventArgs.Text = filterArgs.Content;
if (!FireEvent(PostFilterProcessTimelineStatus, eventArgs)) return;
if (!FireEvent(PreSendMessageTimelineStatus, eventArgs)) return;
// 送信先を決定する
List routedGroups = RoutingStatusMessage(status, eventArgs.Text, eventArgs.IRCMessageType);
// メインチャンネルを送信先として追加する
routedGroups.Add(new RoutedGroup()
{
Group = new Group(_config.ChannelName),
IRCMessageType = eventArgs.IRCMessageType,
IsExistsInChannelOrNoMembers = true,
IsMessageFromSelf = false,
Text = eventArgs.Text
});
if (!FireEvent(MessageRoutedTimelineStatus, new TimelineStatusRoutedEventArgs(status, eventArgs.Text, routedGroups))) return;
// 送信する
foreach (RoutedGroup routedGroup in routedGroups)
{
TimelineStatusGroupEventArgs eventArgsGroup = new TimelineStatusGroupEventArgs(status, routedGroup.Text, routedGroup.IRCMessageType, routedGroup.Group);
if (!FireEvent(PreSendGroupMessageTimelineStatus, eventArgsGroup)) return;
String[] lines = eventArgsGroup.Text.Split(new Char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (isFirstTime && !_config.DisableNoticeAtFirstTime)
{
// 初回のときはNOTICE+時間
Send(CreateIRCMessageFromStatusAndType(status, "NOTICE", routedGroup.Group.Name,
String.Format("{0}: {1}",
status.CreatedAt.ToString("HH:mm"), line)));
}
else
{
Send(CreateIRCMessageFromStatusAndType(status, eventArgsGroup.IRCMessageType,
routedGroup.Group.Name, line));
}
}
if (!FireEvent(PostSendGroupMessageTimelineStatus, eventArgsGroup)) return;
}
if (!FireEvent(PostSendMessageTimelineStatus, eventArgs)) return;
if (!FireEvent(PostProcessTimelineStatus, eventArgs)) return;
}
///
/// メッセージを送信する先を決定します
///
///
///
///
///
public List RoutingStatusMessage(Status status, String text, String ircMessageType)
{
List routedGroups = new List();
foreach (Group group in _groups.Values)
{
if (!group.IsJoined || !group.IsRoutable)
continue;
Boolean isOrMatch = group.IsOrMatch;
Boolean isMatched = String.IsNullOrEmpty(group.Topic) ? true : Regex.IsMatch(text, (isOrMatch ? group.Topic.Substring(1) : group.Topic));
Boolean isExistsInChannelOrNoMembers = (group.Exists(status.User.ScreenName) || group.Members.Count == 0);
Boolean isMessageFromSelf = (_twitterUser != null) ? (status.User.Id == _twitterUser.Id && !group.IgnoreEchoBack) : false;
// 0: self && !IgnoreEchoback
// 1: member exists in channel && match regex
// 2: no members in channel(self only) && match regex
// 3: member exists in channel || match regex (StartsWith: "|")
// 4: no members in channel(self only) || match regex (StartsWith: "|")
if (isMessageFromSelf || (group.IsOrMatch ? (isExistsInChannelOrNoMembers || isMatched) : (isExistsInChannelOrNoMembers && isMatched)))
{
routedGroups.Add(new RoutedGroup()
{
Group = group,
IsExistsInChannelOrNoMembers = isExistsInChannelOrNoMembers,
IsMessageFromSelf = isMessageFromSelf,
// 自分からのメッセージでBroadcastUpdateMessageIsNoticeがTrueのときはNOTICE
IRCMessageType = (isMessageFromSelf && _config.BroadcastUpdateMessageIsNotice) ? "NOTICE" : ircMessageType,
Text = text
});
}
}
return routedGroups;
}
// XXX: IRCクライアントライブラリのアップデートで対応できるけどとりあえず...
private IRCMessage CreateIRCMessageFromStatusAndType(Status status, String type, String receiver, String line)
{
IRCMessage msg;
switch (type.ToUpperInvariant())
{
case "NOTICE":
msg = new NoticeMessage(receiver, line);
break;
case "PRIVMSG":
default:
msg = new PrivMsgMessage(receiver, line);
break;
}
msg.SenderNick = status.User.ScreenName;
msg.SenderHost = "twitter@" + Server.ServerName;
return msg;
}
///
/// チェックを実行します。例外が発生した場合には自動的にメッセージを送信します。
///
/// 実行するチェック処理
///
public Boolean RunCheck(Procedure proc)
{
try
{
proc();
}
catch (WebException ex)
{
if (ex.Response == null || !(ex.Response is HttpWebResponse) || ((HttpWebResponse)(ex.Response)).StatusCode != HttpStatusCode.NotModified)
{
// not-modified 以外
twitter_CheckError(_twitter, new ErrorEventArgs(ex));
return false;
}
}
catch (TwitterServiceException ex2)
{
twitter_CheckError(_twitter, new ErrorEventArgs(ex2));
return false;
}
return true;
}
///
/// チェックを実行します。Twitterに由来する例外が発生した場合には指定したデリゲートを呼び出します。
///
/// 実行するチェック処理
///
public Boolean RunCheck(Procedure proc, Action twitterExceptionCallback)
{
try
{
proc();
}
catch (WebException ex)
{
if (ex.Response == null || !(ex.Response is HttpWebResponse) || ((HttpWebResponse)(ex.Response)).StatusCode != HttpStatusCode.NotModified)
{
// not-modified 以外
twitterExceptionCallback(ex);
return false;
}
}
catch (TwitterServiceException ex2)
{
twitterExceptionCallback(ex2);
return false;
}
return true;
}
///
///
///
protected override void OnClosing()
{
OnSessionEnded();
Dispose();
base.OnClosing();
}
///
///
///
private void CheckDisposed()
{
if (_isDisposed)
throw new ObjectDisposedException(this.GetType().Name);
}
public override string ToString()
{
return String.Format("Session: User={0}, Connections=[{1}]", _twitterUser.ScreenName, String.Join(", ", (from conn in Connections select conn.UserInfo.EndPoint.ToString()).ToArray()));
}
#region ヘルパーメソッド
///
///
///
///
///
///
/// キャンセルされた場合にはfalseが返ります。
[DebuggerStepThrough]
private Boolean FireEvent(EventHandler handlers, TEventArgs e) where TEventArgs:EventArgs
{
if (handlers != null)
{
foreach (EventHandler eventHandler in handlers.GetInvocationList())
{
try
{
eventHandler(this, e);
}
catch (Exception ex)
{
Logger.Error(ex.ToString());
}
}
}
if (e is CancelableEventArgs)
{
return !((e as CancelableEventArgs).Cancel);
}
return true;
}
#endregion
#region IDisposable メンバ
private Boolean _isDisposed = false;
public void Dispose()
{
if (!_isDisposed)
{
try
{
if (AddInManager != null)
AddInManager.Uninitialize();
}
catch {}
if (_config.EnableTrace)
{
#if FALSE
Trace.Listeners.Remove(_traceListener);
#endif
}
if (_twitter != null)
{
_twitter.Dispose();
_twitter = null;
}
GC.SuppressFinalize(this);
_Counter.Decrement(ref _Counter.Session);
_isDisposed = true;
}
}
#endregion
protected override void OnAttached(ConnectionBase connection)
{
lock (this)
{
// セッションを初期化
if (!IsStarted)
InitializeSession();
// メインチャンネルにJOIN
SendServer(new JoinMessage(_config.ChannelName, ""));
// ユーザ一覧
if (_followingUsers.Count > 0)
{
ShowChannelUsers(connection);
}
// グループにJOIN
foreach (Group group in Groups.Values)
{
if (group.IsJoined)
{
JoinChannel(connection, group);
}
}
// 開始
if (!IsStarted)
Start();
}
}
protected override void OnDetached(ConnectionBase connection)
{
}
protected override void OnMessageReceivedFromClient(MessageReceivedEventArgs e)
{
// 転送する
OnMessageReceived(e.Message, e);
}
}
public delegate void Procedure();
}