[smuxi] Engine-Twitter: implemented search streams



commit 38ab881a6da58187e903b34cd2c5d7b7349acfad
Author: Mirco Bauer <meebey meebey net>
Date:   Sun Nov 10 01:33:10 2013 +0100

    Engine-Twitter: implemented search streams

 lib/Makefile.am                                    |   16 ++-
 src/Common-Tests/Common-Tests.csproj               |    1 +
 src/Common-Tests/RateLimiterTests.cs               |   57 +++++++
 src/Common/Common.csproj                           |    1 +
 src/Common/Makefile.am                             |    1 +
 src/Common/RateLimiter.cs                          |   82 ++++++++++
 src/Engine-Twitter/Engine-Twitter.csproj           |    5 +
 src/Engine-Twitter/Makefile.am                     |   11 +-
 .../Protocols/Twitter/TwitterProtocolManager.cs    |   22 +++-
 .../Protocols/Twitter/TwitterSearchStream.cs       |  161 ++++++++++++++++++++
 src/smuxi.sln                                      |   14 ++
 11 files changed, 365 insertions(+), 6 deletions(-)
---
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 017cfc2..7b95015 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -76,6 +76,14 @@ TWITTERIZER_ASSEMBLY_TARGET = $(OUTPUT_DIR)/$(TWITTERIZER_ASSEMBLY_NAME)
 TWITTERIZER_BUILD_FILE = $(TWITTERIZER_SRCDIR)/Twitterizer2.csproj
 TWITTERIZER_XBUILD_FLAGS = $(XBUILD_FLAGS) /p:PostBuildEvent=
 
+TWITTERIZER_STREAMING_SRCDIR = $(srcdir)/$(TWITTERIZER_SUBDIR)/Twitterizer2.Streaming
+TWITTERIZER_STREAMING_SOURCE_FILES = $(foreach pattern, ../*.cs $(SOURCE_PATTERNS), $(wildcard 
$(TWITTERIZER_STREAMING_SRCDIR)/$(pattern)))
+TWITTERIZER_STREAMING_EXTRA_FILES = $(TWITTERIZER_STREAMING_BUILD_FILE)
+TWITTERIZER_STREAMING_ASSEMBLY_NAME = Twitterizer2.Streaming.dll
+TWITTERIZER_STREAMING_ASSEMBLY_TARGET = $(OUTPUT_DIR)/$(TWITTERIZER_STREAMING_ASSEMBLY_NAME)
+TWITTERIZER_STREAMING_BUILD_FILE = $(TWITTERIZER_STREAMING_SRCDIR)/Twitterizer2.Streaming.csproj
+TWITTERIZER_STREAMING_XBUILD_FLAGS = $(XBUILD_FLAGS)
+
 JABBER_NET_SUBDIR = agsxmpp/agsxmpp
 JABBER_NET_SRCDIR = $(srcdir)/$(JABBER_NET_SUBDIR)
 JABBER_NET_SOURCE_FILES = $(foreach pattern, $(SOURCE_PATTERNS), $(wildcard $(JABBER_NET_SRCDIR)/$(pattern)))
@@ -161,7 +169,8 @@ if ENABLE_ENGINE_TWITTER
 EXTRA_JSON_LIBS = \
        $(JSON_ASSEMBLY_TARGET) $(JSON_ASSEMBLY_TARGET).mdb
 EXTRA_TWITTER_LIBS = \
-       $(TWITTERIZER_ASSEMBLY_TARGET) $(TWITTERIZER_ASSEMBLY_TARGET).mdb
+       $(TWITTERIZER_ASSEMBLY_TARGET) $(TWITTERIZER_ASSEMBLY_TARGET).mdb \
+       $(TWITTERIZER_STREAMING_ASSEMBLY_TARGET) $(TWITTERIZER_STREAMING_ASSEMBLY_TARGET).mdb
 endif
 if ENABLE_ENGINE_XMPP
 EXTRA_XMPP_LIBS = \
@@ -206,6 +215,7 @@ EXTRA_DIST = \
        $(SERVICESTACK_COMMON_SRCDIR) $(SERVICESTACK_COMMON_EXTRA_FILES) \
        $(JSON_SRCDIR) $(JSON_EXTRA_FILES) \
        $(TWITTERIZER_SRCDIR) $(TWITTERIZER_EXTRA_FILES) \
+       $(TWITTERIZER_STREAMING_SRCDIR) $(TWITTERIZER_STREAMING_EXTRA_FILES) \
        $(JABBER_NET_SRCDIR) $(JABBER_NET_EXTRA_FILES) \
        $(DB4O_SRCDIR) $(DB4O_EXTRA_FILES) \
        $(STARKSOFTPROXY_SRCDIR) $(STARKSOFTPROXY_EXTRA_FILES) \
@@ -248,6 +258,9 @@ $(JSON_ASSEMBLY_TARGET) $(JSON_ASSEMBLY_TARGET).mdb: $(JSON_BUILD_FILE) $(JSON_S
 $(TWITTERIZER_ASSEMBLY_TARGET) $(TWITTERIZER_ASSEMBLY_TARGET).mdb: $(TWITTERIZER_BUILD_FILE) 
$(TWITTERIZER_SOURCE_FILES)
        $(XBUILD) $(TWITTERIZER_XBUILD_FLAGS) $(TWITTERIZER_BUILD_FILE)
 
+$(TWITTERIZER_STREAMING_ASSEMBLY_TARGET) $(TWITTERIZER_STREAMING_ASSEMBLY_TARGET).mdb: 
$(TWITTERIZER_STREAMING_BUILD_FILE) $(TWITTERIZER_STREAMING_SOURCE_FILES)
+       $(XBUILD) $(TWITTERIZER_STREAMING_XBUILD_FLAGS) $(TWITTERIZER_STREAMING_BUILD_FILE)
+
 $(JABBER_NET_ASSEMBLY_TARGET) $(JABBER_NET_ASSEMBLY_TARGET).mdb: $(JABBER_NET_BUILD_FILE) 
$(JABBER_NET_SOURCE_FILES)
        $(XBUILD) $(JABBER_NET_XBUILD_FLAGS) $(JABBER_NET_BUILD_FILE)
        
@@ -283,6 +296,7 @@ endif
 if ENABLE_ENGINE_TWITTER
        $(XBUILD) $(JSON_XBUILD_FLAGS) /t:Clean $(JSON_BUILD_FILE)
        $(XBUILD) $(TWITTERIZER_XBUILD_FLAGS) /t:Clean $(TWITTERIZER_BUILD_FILE)
+       $(XBUILD) $(TWITTERIZER_STREAMING_XBUILD_FLAGS) /t:Clean $(TWITTERIZER_STREAMING_BUILD_FILE)
 endif
 if ENABLE_ENGINE_XMPP
        $(XBUILD) $(JABBER_NET_XBUILD_FLAGS) /t:Clean $(JABBER_NET_BUILD_FILE)
diff --git a/src/Common-Tests/Common-Tests.csproj b/src/Common-Tests/Common-Tests.csproj
index f588a78..447906a 100644
--- a/src/Common-Tests/Common-Tests.csproj
+++ b/src/Common-Tests/Common-Tests.csproj
@@ -36,6 +36,7 @@
     <Compile Include="TraceTests.cs" />
     <Compile Include="PatternTests.cs" />
     <Compile Include="Crc32Tests.cs" />
+    <Compile Include="RateLimiterTests.cs" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="System" />
diff --git a/src/Common-Tests/RateLimiterTests.cs b/src/Common-Tests/RateLimiterTests.cs
new file mode 100644
index 0000000..4000816
--- /dev/null
+++ b/src/Common-Tests/RateLimiterTests.cs
@@ -0,0 +1,57 @@
+// Smuxi - Smart MUltipleXed Irc
+//
+// Copyright (c) 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 NUnit.Framework;
+using System.Threading;
+
+namespace Smuxi.Common
+{
+    [TestFixture]
+    public class RateLimiterTests
+    {
+        [Test]
+        public void AboveLimit()
+        {
+            var limiter = new RateLimiter(10, TimeSpan.FromSeconds(10));
+            for (int i = 0; i < 100; i++) {
+                if (limiter.IsRateLimited) {
+                    Assert.AreEqual(10, i);
+                    break;
+                }
+                limiter++;
+            }
+        }
+
+        [Test]
+        public void BelowLimit()
+        {
+            var limiter = new RateLimiter(10, TimeSpan.FromMilliseconds(10));
+            for (int i = 0; i < 100; i++) {
+                if (limiter.IsRateLimited) {
+                    break;
+                }
+                limiter++;
+            }
+            Thread.Sleep(10);
+            Assert.IsFalse(limiter.IsRateLimited);
+        }
+    }
+}
+
diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj
index 220cf30..ce9f98f 100644
--- a/src/Common/Common.csproj
+++ b/src/Common/Common.csproj
@@ -57,6 +57,7 @@
     <Compile Include="ThreadPoolQueue.cs" />
     <Compile Include="AtomFeed.cs" />
     <Compile Include="SpecialFolderPatternConverter.cs" />
+    <Compile Include="RateLimiter.cs" />
   </ItemGroup>
   <ItemGroup>
     <None Include="Defines.cs.in" />
diff --git a/src/Common/Makefile.am b/src/Common/Makefile.am
index 359adfb..045df52 100644
--- a/src/Common/Makefile.am
+++ b/src/Common/Makefile.am
@@ -62,6 +62,7 @@ FILES = \
        TaskQueue.cs \
        ThreadPoolQueue.cs \
        Platform.cs \
+       RateLimiter.cs \
        Pattern.cs 
        
 DATA_FILES = 
diff --git a/src/Common/RateLimiter.cs b/src/Common/RateLimiter.cs
new file mode 100644
index 0000000..6ea8458
--- /dev/null
+++ b/src/Common/RateLimiter.cs
@@ -0,0 +1,82 @@
+// This file is part of Smuxi and is licensed under the terms of MIT/X11
+//
+// Copyright (c) 2014 Mirco Bauer <meebey meebey net>
+// Copyright (c) 2014 Oliver Schneider <smuxi oli-obk de>
+//
+// 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.
+using System;
+
+namespace Smuxi.Common
+{
+    public class RateLimiter
+    {
+        TimeSpan TimeWindow { get; set; }
+        int CallCounter { get; set; }
+        int CallLimit { get; set; }
+        DateTime FirstCall { get; set; }
+        object SyncRoot { get; set; }
+
+        public bool IsRateLimited {
+            get {
+                return IsAboveLimit && IsInWindow;
+            }
+        }
+
+        bool IsAboveLimit {
+            get {
+                return CallLimit <= CallCounter;
+            }
+        }
+
+        bool IsInWindow {
+            get {
+                return (DateTime.UtcNow - FirstCall) < TimeWindow;
+            }
+        }
+
+        public RateLimiter(int callLimit, TimeSpan timeWindow)
+        {
+            if (callLimit <= 0) {
+                throw new ArgumentException("callLimit must be greater than 0.", "callLimit");
+            }
+
+            CallLimit = callLimit;
+            TimeWindow = timeWindow;
+            SyncRoot = new object();
+        }
+
+        public static RateLimiter operator ++(RateLimiter l)
+        {
+            if (l.IsRateLimited) {
+                throw new InvalidOperationException("IsRateLimited must not be true.");
+            }
+
+            lock (l.SyncRoot) {
+                if (!l.IsInWindow) {
+                    l.CallCounter = 0;
+                    l.FirstCall = DateTime.UtcNow;
+                }
+
+                l.CallCounter++;
+            }
+            return l;
+        }
+    }
+}
+
diff --git a/src/Engine-Twitter/Engine-Twitter.csproj b/src/Engine-Twitter/Engine-Twitter.csproj
index 6923cd1..28e3605 100644
--- a/src/Engine-Twitter/Engine-Twitter.csproj
+++ b/src/Engine-Twitter/Engine-Twitter.csproj
@@ -35,6 +35,7 @@
     <Compile Include="AssemblyInfo.cs" />
     <Compile Include="Protocols\Twitter\TwitterProtocolManager.cs" />
     <Compile Include="Protocols\Twitter\TwitterMessageBuilder.cs" />
+    <Compile Include="Protocols\Twitter\TwitterSearchStream.cs" />
   </ItemGroup>
   <ItemGroup>
     <Folder Include="Protocols\" />
@@ -61,6 +62,10 @@
       <Project>{2FDC3492-6B9E-4771-9755-7892C9CB1E96}</Project>
       <Name>Twitterizer2</Name>
     </ProjectReference>
+    <ProjectReference Include="..\..\lib\Twitterizer\Twitterizer2.Streaming\Twitterizer2.Streaming.csproj">
+      <Project>{AFDC57ED-C013-4581-9D06-54D8A42D696D}</Project>
+      <Name>Twitterizer2.Streaming</Name>
+    </ProjectReference>
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
 </Project>
\ No newline at end of file
diff --git a/src/Engine-Twitter/Makefile.am b/src/Engine-Twitter/Makefile.am
index f51ed5b..f476c90 100644
--- a/src/Engine-Twitter/Makefile.am
+++ b/src/Engine-Twitter/Makefile.am
@@ -7,16 +7,19 @@ ASSEMBLY_TARGET = $(TARGET_DIR)/$(ASSEMBLY_FILENAME)
 SOURCES =      $(top_srcdir)/src/AssemblyVersion.cs \
                AssemblyInfo.cs \
                Protocols/Twitter/TwitterProtocolManager.cs \
-               Protocols/Twitter/TwitterMessageBuilder.cs
+               Protocols/Twitter/TwitterMessageBuilder.cs \
+               Protocols/Twitter/TwitterSearchStream.cs
 
 REFERENCES =           $(LOG4NET_LIBS) \
                        $(SMARTIRC4NET_LIBS) \
                        System.Web \
                        System.Core
 
-DLL_REFERENCES =       $(TARGET_DIR)/Twitterizer2.dll \
-                       $(TARGET_DIR)/smuxi-common.dll \
-                       $(TARGET_DIR)/smuxi-engine.dll
+DLL_REFERENCES = \
+       $(TARGET_DIR)/Twitterizer2.dll \
+       $(TARGET_DIR)/Twitterizer2.Streaming.dll \
+       $(TARGET_DIR)/smuxi-common.dll \
+       $(TARGET_DIR)/smuxi-engine.dll
 
 SOURCES_BUILD = $(addprefix $(srcdir)/, $(SOURCES))
 
diff --git a/src/Engine-Twitter/Protocols/Twitter/TwitterProtocolManager.cs 
b/src/Engine-Twitter/Protocols/Twitter/TwitterProtocolManager.cs
index f56635e..aa16c73 100644
--- a/src/Engine-Twitter/Protocols/Twitter/TwitterProtocolManager.cs
+++ b/src/Engine-Twitter/Protocols/Twitter/TwitterProtocolManager.cs
@@ -84,6 +84,7 @@ namespace Smuxi.Engine
 
         TwitterStatus[]         StatusIndex { get; set; }
         int                     StatusIndexOffset { get; set; }
+        Dictionary<string, TwitterSearchStream> SearchStreams { get; set; }
 
         public override string NetworkID {
             get {
@@ -153,6 +154,7 @@ namespace Smuxi.Engine
             f_GroupChats.Add(f_DirectMessagesChat);
 
             StatusIndex = new TwitterStatus[99];
+            SearchStreams = new Dictionary<string, TwitterSearchStream>();
         }
 
         public override void Connect(FrontendManager fm, ServerModel server)
@@ -206,6 +208,9 @@ namespace Smuxi.Engine
                     if (!whitelist.Contains("api.twitter.com")) {
                         whitelist.Add("api.twitter.com");
                     }
+                    if (!whitelist.Contains("stream.twitter.com")) {
+                        whitelist.Add("stream.twitter.com");
+                    }
                 }
             }
 
@@ -605,6 +610,15 @@ namespace Smuxi.Engine
                         }
                         break;
                 }
+            } else {
+                // no static/singleton chat, but maybe a search?
+                TwitterSearchStream stream;
+                lock (SearchStreams) {
+                    if (SearchStreams.TryGetValue(chat.ID, out stream)) {
+                        SearchStreams.Remove(chat.ID);
+                        stream.Dispose();
+                    }
+                }
             }
 
             Session.RemoveChat(chat);
@@ -1102,6 +1116,12 @@ namespace Smuxi.Engine
                 }
             }
             Session.SyncChat(chat);
+
+            var stream = new TwitterSearchStream(this, chat, keyword,
+                                                 f_OAuthTokens, f_WebProxy);
+            lock (SearchStreams) {
+                SearchStreams.Add(chat.ID, stream);
+            }
         }
 
         public void CommandRetweet(CommandModel cmd)
@@ -1881,7 +1901,7 @@ namespace Smuxi.Engine
             return false;
         }
 
-        private PersonModel GetPerson(TwitterUser user)
+        internal PersonModel GetPerson(TwitterUser user)
         {
             if (user == null) {
                 throw new ArgumentNullException("user");
diff --git a/src/Engine-Twitter/Protocols/Twitter/TwitterSearchStream.cs 
b/src/Engine-Twitter/Protocols/Twitter/TwitterSearchStream.cs
new file mode 100644
index 0000000..858ec7d
--- /dev/null
+++ b/src/Engine-Twitter/Protocols/Twitter/TwitterSearchStream.cs
@@ -0,0 +1,161 @@
+// Smuxi - Smart MUltipleXed Irc
+//
+// Copyright (c) 2013-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.Net;
+using Twitterizer;
+using Twitterizer.Streaming;
+using Smuxi.Common;
+
+namespace Smuxi.Engine
+{
+    public class TwitterSearchStream : IDisposable
+    {
+#if LOG4NET
+        static readonly log4net.ILog Logger = 
log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
+#endif
+        TwitterProtocolManager ProtocolManager { get; set; }
+        Session Session { get; set; }
+        TwitterStream Stream { get; set; }
+        GroupChatModel Chat { get; set; }
+        RateLimiter MessageRateLimiter { get; set; }
+
+        public TwitterSearchStream(TwitterProtocolManager protocolManager,
+                                   GroupChatModel chat, string keyword,
+                                   OAuthTokens tokens, WebProxy proxy)
+        {
+            if (protocolManager == null) {
+                throw new ArgumentNullException("protocolManager");
+            }
+            if (chat == null) {
+                throw new ArgumentNullException("chat");
+            }
+            if (keyword == null) {
+                throw new ArgumentNullException("keyword");
+            }
+            if (tokens == null) {
+                throw new ArgumentNullException("tokens");
+            }
+
+            ProtocolManager = protocolManager;
+            Session = protocolManager.Session;
+            Chat = chat;
+
+            var options = new StreamOptions();
+            options.Track.Add(keyword);
+
+            Stream = new TwitterStream(tokens, null, options);
+            Stream.Proxy = proxy;
+            Stream.StartPublicStream(OnStreamStopped, OnStatusCreated, OnStatusDeleted, OnEvent);
+
+            MessageRateLimiter = new RateLimiter(5, TimeSpan.FromSeconds(5));
+        }
+
+        ~TwitterSearchStream()
+        {
+            Dispose(false);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            Trace.Call(disposing);
+
+            Stream.EndStream();
+            Stream.Dispose();
+        }
+
+        public void Dispose()
+        {
+            GC.SuppressFinalize(this);
+            Dispose(true);
+        }
+
+        protected TwitterMessageBuilder CreateMessageBuilder()
+        {
+            var builder = new TwitterMessageBuilder();
+            builder.ApplyConfig(Session.UserConfig);
+            return builder;
+        }
+
+        void OnStreamStopped(StopReasons reason)
+        {
+            Trace.Call(reason);
+
+            try {
+                Session.DisableChat(Chat);
+            } catch (Exception ex) {
+#if LOG4NET
+                Logger.Error("OnStreamStopped()", ex);
+#endif
+            }
+        }
+
+        void OnStatusCreated(TwitterStatus status)
+        {
+            Trace.Call(status);
+
+            try {
+                if (MessageRateLimiter.IsRateLimited) {
+                    return;
+                }
+                MessageRateLimiter++;
+
+                var sender = ProtocolManager.GetPerson(status.User);
+                var userId = status.User.Id.ToString();
+                lock (Chat.UnsafePersons) {
+                    if (!Chat.UnsafePersons.ContainsKey(userId)) {
+                        Session.AddPersonToGroupChat(Chat, sender);
+                    }
+                }
+                var msg = CreateMessageBuilder().
+                    Append(status, sender).
+                    ToMessage();
+                Session.AddMessageToChat(Chat, msg);
+            } catch (Exception ex) {
+#if LOG4NET
+                Logger.Error("OnStatusCreated()", ex);
+#endif
+            }
+        }
+
+        void OnStatusDeleted(TwitterStreamDeletedEvent status)
+        {
+            Trace.Call(status);
+
+            try {
+            } catch (Exception ex) {
+#if LOG4NET
+                Logger.Error("OnStatusDeleted()", ex);
+#endif
+            }
+        }
+
+        void OnEvent(TwitterStreamEvent @event)
+        {
+            Trace.Call(@event);
+
+            try {
+            } catch (Exception ex) {
+#if LOG4NET
+                Logger.Error("OnEvent()", ex);
+#endif
+            }
+        }
+    }
+}
diff --git a/src/smuxi.sln b/src/smuxi.sln
index f2a5b29..d8f6fc9 100644
--- a/src/smuxi.sln
+++ b/src/smuxi.sln
@@ -69,6 +69,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frontend-GNOME-Twitter", "F
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine-MessageBuffer", 
"Engine-MessageBuffer\Engine-MessageBuffer.csproj", "{7964E3E4-1E98-4AE0-AF1B-1B940CDF0A99}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Twitterizer2.Streaming", 
"..\lib\Twitterizer\Twitterizer2.Streaming\Twitterizer2.Streaming.csproj", 
"{AFDC57ED-C013-4581-9D06-54D8A42D696D}"
+EndProject
 Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                Release|Any CPU = Release|Any CPU
@@ -151,6 +153,8 @@ Global
                {3F8CF2C1-EA37-444F-8693-A3A00B1131D2}.Release-Win32|x86.Build.0 = Release|Any CPU
                {405874F0-A58A-4E79-8766-2150D069F691}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
                {405874F0-A58A-4E79-8766-2150D069F691}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {405874F0-A58A-4E79-8766-2150D069F691}.Debug-Win32|Any CPU.ActiveCfg = Release|Any CPU
+               {405874F0-A58A-4E79-8766-2150D069F691}.Debug-Win32|Any CPU.Build.0 = Release|Any CPU
                {405874F0-A58A-4E79-8766-2150D069F691}.Debug-Win32|x86.ActiveCfg = Debug-Win32|x86
                {405874F0-A58A-4E79-8766-2150D069F691}.Debug-Win32|x86.Build.0 = Debug-Win32|x86
                {405874F0-A58A-4E79-8766-2150D069F691}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -281,6 +285,16 @@ Global
                {AB347741-B8CC-4B6B-BD39-826C77BE0694}.Debug|Any CPU.Build.0 = Debug|Any CPU
                {AB347741-B8CC-4B6B-BD39-826C77BE0694}.Release|Any CPU.ActiveCfg = Release|Any CPU
                {AB347741-B8CC-4B6B-BD39-826C77BE0694}.Release|Any CPU.Build.0 = Release|Any CPU
+               {AFDC57ED-C013-4581-9D06-54D8A42D696D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {AFDC57ED-C013-4581-9D06-54D8A42D696D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {AFDC57ED-C013-4581-9D06-54D8A42D696D}.Debug-Win32|Any CPU.ActiveCfg = Release|Any CPU
+               {AFDC57ED-C013-4581-9D06-54D8A42D696D}.Debug-Win32|Any CPU.Build.0 = Release|Any CPU
+               {AFDC57ED-C013-4581-9D06-54D8A42D696D}.Debug-Win32|x86.ActiveCfg = Debug|Any CPU
+               {AFDC57ED-C013-4581-9D06-54D8A42D696D}.Debug-Win32|x86.Build.0 = Debug|Any CPU
+               {AFDC57ED-C013-4581-9D06-54D8A42D696D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {AFDC57ED-C013-4581-9D06-54D8A42D696D}.Release|Any CPU.Build.0 = Release|Any CPU
+               {AFDC57ED-C013-4581-9D06-54D8A42D696D}.Release-Win32|x86.ActiveCfg = Release|Any CPU
+               {AFDC57ED-C013-4581-9D06-54D8A42D696D}.Release-Win32|x86.Build.0 = Release|Any CPU
                {B68687B3-E45A-410E-BAE6-A7B2590E17B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
                {B68687B3-E45A-410E-BAE6-A7B2590E17B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
                {B68687B3-E45A-410E-BAE6-A7B2590E17B1}.Debug-Win32|x86.ActiveCfg = Debug-Win32|x86


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]