├── .gitattributes ├── .gitignore ├── Bootstrap.cs ├── Configuration.cs ├── LICENSE ├── README.md ├── SteamToTwitter.csproj ├── SteamToTwitter.sln ├── TinyTwitter.cs └── settings.json.example /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # User-specific files 6 | *.suo 7 | *.user 8 | *.userprefs 9 | *.sln.docstates 10 | *.dll 11 | 12 | # Build results 13 | [Dd]ebug/ 14 | [Rr]elease/ 15 | x64/ 16 | *_i.c 17 | *_p.c 18 | *.ilk 19 | *.meta 20 | *.obj 21 | *.pch 22 | *.pdb 23 | *.pgc 24 | *.pgd 25 | *.rsp 26 | *.sbr 27 | *.tlb 28 | *.tli 29 | *.tlh 30 | *.tmp 31 | *.log 32 | *.vspscc 33 | *.vssscc 34 | .builds 35 | 36 | # NuGet Packages Directory 37 | packages 38 | 39 | # Others 40 | [Bb]in 41 | [Oo]bj 42 | sql 43 | TestResults 44 | [Tt]est[Rr]esult* 45 | *.Cache 46 | ClientBin 47 | [Ss]tyle[Cc]op.* 48 | ~$* 49 | *.dbmdl 50 | .vs/ 51 | 52 | # Backup & report files from converting an old project file to a newer 53 | # Visual Studio version. Backup files are not needed, because we have git ;-) 54 | _UpgradeReport_Files/ 55 | Backup*/ 56 | UpgradeLog*.XML 57 | 58 | settings.json 59 | sentry.bin 60 | -------------------------------------------------------------------------------- /Bootstrap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.Json; 4 | using System.Threading; 5 | using SteamKit2; 6 | 7 | namespace SteamToTwitter 8 | { 9 | internal static class Bootstrap 10 | { 11 | private static readonly SteamClient Client = new SteamClient(); 12 | private static readonly SteamUser User = Client.GetHandler(); 13 | private static readonly SteamFriends Friends = Client.GetHandler(); 14 | private static Timer ReconnectTimer; 15 | private static bool IsRunning = true; 16 | private static TinyTwitter.TinyTwitter Twitter; 17 | private static Configuration Configuration; 18 | private static string authCode, twoFactorAuth; 19 | 20 | public static void Main() 21 | { 22 | Console.Title = "SteamToTwitter"; 23 | 24 | Log("Starting..."); 25 | 26 | Console.CancelKeyPress += delegate 27 | { 28 | Log("Exiting..."); 29 | 30 | try 31 | { 32 | User.LogOff(); 33 | Client.Disconnect(); 34 | } 35 | catch 36 | { 37 | Log("Failed to disconnect from Steam"); 38 | } 39 | 40 | IsRunning = false; 41 | }; 42 | 43 | Configuration = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "settings.json"))); 44 | 45 | Twitter = new TinyTwitter.TinyTwitter(Configuration.Twitter); 46 | 47 | var callbackManager = new CallbackManager(Client); 48 | 49 | callbackManager.Subscribe(OnConnected); 50 | callbackManager.Subscribe(OnDisconnected); 51 | callbackManager.Subscribe(OnLoggedOn); 52 | callbackManager.Subscribe(OnLoggedOff); 53 | callbackManager.Subscribe(OnMachineAuth); 54 | callbackManager.Subscribe(OnAccountInfo); 55 | callbackManager.Subscribe(OnClanState); 56 | 57 | Client.Connect(); 58 | 59 | var reconnectTime = TimeSpan.FromHours(6); 60 | ReconnectTimer = new Timer(_ => Client.Disconnect(), null, reconnectTime, reconnectTime); 61 | 62 | while (IsRunning) 63 | { 64 | callbackManager.RunWaitCallbacks(TimeSpan.FromSeconds(5)); 65 | } 66 | } 67 | 68 | private static void OnConnected(SteamClient.ConnectedCallback callback) 69 | { 70 | Log("Connected to Steam, logging in..."); 71 | 72 | byte[] sentryHash = null; 73 | 74 | if (File.Exists("sentry.bin")) 75 | { 76 | var sentryFile = File.ReadAllBytes("sentry.bin"); 77 | sentryHash = CryptoHelper.SHAHash(sentryFile); 78 | } 79 | 80 | User.LogOn(new SteamUser.LogOnDetails 81 | { 82 | AuthCode = authCode, 83 | TwoFactorCode = twoFactorAuth, 84 | SentryFileHash = sentryHash, 85 | Username = Configuration.SteamUsername, 86 | Password = Configuration.SteamPassword 87 | }); 88 | } 89 | 90 | private static void OnDisconnected(SteamClient.DisconnectedCallback callback) 91 | { 92 | if (!IsRunning) 93 | { 94 | Log("Shutting down..."); 95 | 96 | return; 97 | } 98 | 99 | Log("Disconnected from Steam. Retrying..."); 100 | 101 | Thread.Sleep(TimeSpan.FromSeconds(15)); 102 | 103 | Client.Connect(); 104 | } 105 | 106 | private static void OnLoggedOn(SteamUser.LoggedOnCallback callback) 107 | { 108 | if (callback.Result == EResult.AccountLoginDeniedNeedTwoFactor) 109 | { 110 | Console.Write("Please enter your 2 factor auth code from your authenticator app: "); 111 | twoFactorAuth = Console.ReadLine(); 112 | return; 113 | } 114 | 115 | if (callback.Result == EResult.AccountLogonDenied) 116 | { 117 | Console.Write("Please enter the auth code sent to the email at {0}: ", callback.EmailDomain); 118 | authCode = Console.ReadLine(); 119 | return; 120 | } 121 | 122 | if (callback.Result != EResult.OK) 123 | { 124 | Log($"Failed to login: {callback.Result}"); 125 | 126 | Thread.Sleep(TimeSpan.FromSeconds(2)); 127 | 128 | return; 129 | } 130 | 131 | Log($"Logged in, current valve time is {callback.ServerTime} UTC"); 132 | } 133 | 134 | private static void OnLoggedOff(SteamUser.LoggedOffCallback callback) 135 | { 136 | Log($"Logged off from Steam: {callback.Result}"); 137 | } 138 | 139 | private static void OnAccountInfo(SteamUser.AccountInfoCallback callback) 140 | { 141 | Friends.SetPersonaState(EPersonaState.Busy); 142 | } 143 | 144 | private static void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) 145 | { 146 | Log("Updating sentryfile so that you don't need to authenticate with SteamGuard next time."); 147 | 148 | var sentryHash = CryptoHelper.SHAHash(callback.Data); 149 | 150 | File.WriteAllBytes("sentry.bin", callback.Data); 151 | 152 | User.SendMachineAuthResponse(new SteamUser.MachineAuthDetails 153 | { 154 | JobID = callback.JobID, 155 | FileName = callback.FileName, 156 | BytesWritten = callback.BytesToWrite, 157 | FileSize = callback.Data.Length, 158 | Offset = callback.Offset, 159 | Result = EResult.OK, 160 | LastError = 0, 161 | OneTimePassword = callback.OneTimePassword, 162 | SentryFileHash = sentryHash, 163 | }); 164 | } 165 | 166 | public static void OnClanState(SteamFriends.ClanStateCallback callback) 167 | { 168 | if (callback.Announcements.Count == 0) 169 | { 170 | return; 171 | } 172 | 173 | var groupName = callback.ClanName; 174 | 175 | if (string.IsNullOrEmpty(groupName)) 176 | { 177 | groupName = Friends.GetClanName(callback.ClanID); 178 | } 179 | 180 | foreach (var announcement in callback.Announcements) 181 | { 182 | var message = announcement.Headline.Trim(); 183 | 184 | if (!string.IsNullOrEmpty(groupName) && !announcement.Headline.Contains(groupName.Replace("Steam", string.Empty).Trim())) 185 | { 186 | message = $"{announcement.Headline} ({groupName})"; 187 | } 188 | 189 | // 240 max tweet length, minus 23 characters for the t.co link 190 | if (message.Length > 217) 191 | { 192 | message = $"{message.Substring(0, 216)}…"; 193 | } 194 | 195 | var url = $"https://steamcommunity.com/gid/{callback.ClanID.ConvertToUInt64()}/announcements/detail/{announcement.ID}"; 196 | 197 | for (var i = 0; i < 2; i++) 198 | { 199 | try 200 | { 201 | Log($"Tweeting \"{message}\" - {url}"); 202 | 203 | Twitter.UpdateStatus($"{message} {url}"); 204 | 205 | break; 206 | } 207 | catch (Exception e) 208 | { 209 | Log($"Exception: {e.Message}"); 210 | } 211 | } 212 | } 213 | } 214 | 215 | public static void Log(string format) 216 | { 217 | Console.WriteLine($"[{DateTime.Now:R}] {format}"); 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /Configuration.cs: -------------------------------------------------------------------------------- 1 | namespace SteamToTwitter 2 | { 3 | internal class Configuration 4 | { 5 | public TinyTwitter.OAuthInfo Twitter { get; set; } 6 | public string SteamUsername { get; set; } 7 | public string SteamPassword { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 Steam Database 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Steam to Twitter 2 | 3 | This is a simple bot that logins to the Steam network 4 | and listens for group announcements, and then tweets them. 5 | 6 | See [LICENSE](LICENSE) file for license information. 7 | 8 | --- 9 | 10 | As of February 2020, Valve have disabled announcement notifications, 11 | so this app is effectively useless. 12 | -------------------------------------------------------------------------------- /SteamToTwitter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | LatestMajor 5 | Exe 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /SteamToTwitter.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamToTwitter", "SteamToTwitter.csproj", "{4587BA8E-F61F-4221-AF4F-E60FB4F8110A}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x86 = Debug|x86 11 | Release|x86 = Release|x86 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {4587BA8E-F61F-4221-AF4F-E60FB4F8110A}.Debug|x86.ActiveCfg = Debug|Any CPU 15 | {4587BA8E-F61F-4221-AF4F-E60FB4F8110A}.Debug|x86.Build.0 = Debug|Any CPU 16 | {4587BA8E-F61F-4221-AF4F-E60FB4F8110A}.Release|x86.ActiveCfg = Release|Any CPU 17 | {4587BA8E-F61F-4221-AF4F-E60FB4F8110A}.Release|x86.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {9C125862-605B-44FB-84E1-712C247FF5E4} 24 | EndGlobalSection 25 | GlobalSection(MonoDevelopProperties) = preSolution 26 | StartupItem = SteamToTwitter.csproj 27 | Policies = $0 28 | $0.TextStylePolicy = $3 29 | $1.inheritsSet = null 30 | $1.scope = text/x-csharp 31 | $0.CSharpFormattingPolicy = $2 32 | $2.AfterDelegateDeclarationParameterComma = True 33 | $2.inheritsSet = Mono 34 | $2.inheritsScope = text/x-csharp 35 | $2.scope = text/x-csharp 36 | $3.FileWidth = 120 37 | $3.NoTabsAfterNonTabs = True 38 | $3.inheritsSet = VisualStudio 39 | $3.inheritsScope = text/plain 40 | $3.scope = text/plain 41 | $0.StandardHeader = $4 42 | $4.Text = 43 | $4.IncludeInNewFiles = True 44 | $0.NameConventionPolicy = $5 45 | $5.Rules = $6 46 | $6.NamingRule = $26 47 | $7.Name = Namespaces 48 | $7.AffectedEntity = Namespace 49 | $7.VisibilityMask = VisibilityMask 50 | $7.NamingStyle = PascalCase 51 | $7.IncludeInstanceMembers = True 52 | $7.IncludeStaticEntities = True 53 | $8.Name = Types 54 | $8.AffectedEntity = Class, Struct, Enum, Delegate 55 | $8.VisibilityMask = Public 56 | $8.NamingStyle = PascalCase 57 | $8.IncludeInstanceMembers = True 58 | $8.IncludeStaticEntities = True 59 | $9.Name = Interfaces 60 | $9.RequiredPrefixes = $10 61 | $10.String = I 62 | $9.AffectedEntity = Interface 63 | $9.VisibilityMask = Public 64 | $9.NamingStyle = PascalCase 65 | $9.IncludeInstanceMembers = True 66 | $9.IncludeStaticEntities = True 67 | $11.Name = Attributes 68 | $11.RequiredSuffixes = $12 69 | $12.String = Attribute 70 | $11.AffectedEntity = CustomAttributes 71 | $11.VisibilityMask = Public 72 | $11.NamingStyle = PascalCase 73 | $11.IncludeInstanceMembers = True 74 | $11.IncludeStaticEntities = True 75 | $13.Name = Event Arguments 76 | $13.RequiredSuffixes = $14 77 | $14.String = EventArgs 78 | $13.AffectedEntity = CustomEventArgs 79 | $13.VisibilityMask = Public 80 | $13.NamingStyle = PascalCase 81 | $13.IncludeInstanceMembers = True 82 | $13.IncludeStaticEntities = True 83 | $15.Name = Exceptions 84 | $15.RequiredSuffixes = $16 85 | $16.String = Exception 86 | $15.AffectedEntity = CustomExceptions 87 | $15.VisibilityMask = VisibilityMask 88 | $15.NamingStyle = PascalCase 89 | $15.IncludeInstanceMembers = True 90 | $15.IncludeStaticEntities = True 91 | $17.Name = Methods 92 | $17.AffectedEntity = Methods 93 | $17.VisibilityMask = Protected, Public 94 | $17.NamingStyle = PascalCase 95 | $17.IncludeInstanceMembers = True 96 | $17.IncludeStaticEntities = True 97 | $18.Name = Static Readonly Fields 98 | $18.AffectedEntity = ReadonlyField 99 | $18.VisibilityMask = Protected, Public 100 | $18.NamingStyle = PascalCase 101 | $18.IncludeInstanceMembers = False 102 | $18.IncludeStaticEntities = True 103 | $19.Name = Fields 104 | $19.AffectedEntity = Field 105 | $19.VisibilityMask = Protected, Public 106 | $19.NamingStyle = PascalCase 107 | $19.IncludeInstanceMembers = True 108 | $19.IncludeStaticEntities = True 109 | $20.Name = ReadOnly Fields 110 | $20.AffectedEntity = ReadonlyField 111 | $20.VisibilityMask = Protected, Public 112 | $20.NamingStyle = PascalCase 113 | $20.IncludeInstanceMembers = True 114 | $20.IncludeStaticEntities = False 115 | $21.Name = Constant Fields 116 | $21.AffectedEntity = ConstantField 117 | $21.VisibilityMask = Protected, Public 118 | $21.NamingStyle = PascalCase 119 | $21.IncludeInstanceMembers = True 120 | $21.IncludeStaticEntities = True 121 | $22.Name = Properties 122 | $22.AffectedEntity = Property 123 | $22.VisibilityMask = Protected, Public 124 | $22.NamingStyle = PascalCase 125 | $22.IncludeInstanceMembers = True 126 | $22.IncludeStaticEntities = True 127 | $23.Name = Events 128 | $23.AffectedEntity = Event 129 | $23.VisibilityMask = Protected, Public 130 | $23.NamingStyle = PascalCase 131 | $23.IncludeInstanceMembers = True 132 | $23.IncludeStaticEntities = True 133 | $24.Name = Enum Members 134 | $24.AffectedEntity = EnumMember 135 | $24.VisibilityMask = VisibilityMask 136 | $24.NamingStyle = PascalCase 137 | $24.IncludeInstanceMembers = True 138 | $24.IncludeStaticEntities = True 139 | $25.Name = Parameters 140 | $25.AffectedEntity = Parameter 141 | $25.VisibilityMask = VisibilityMask 142 | $25.NamingStyle = CamelCase 143 | $25.IncludeInstanceMembers = True 144 | $25.IncludeStaticEntities = True 145 | $26.Name = Type Parameters 146 | $26.RequiredPrefixes = $27 147 | $27.String = T 148 | $26.AffectedEntity = TypeParameter 149 | $26.VisibilityMask = VisibilityMask 150 | $26.NamingStyle = PascalCase 151 | $26.IncludeInstanceMembers = True 152 | $26.IncludeStaticEntities = True 153 | $0.VersionControlPolicy = $28 154 | $28.inheritsSet = Mono 155 | EndGlobalSection 156 | EndGlobal 157 | -------------------------------------------------------------------------------- /TinyTwitter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | 10 | namespace TinyTwitter 11 | { 12 | public class OAuthInfo 13 | { 14 | public string ConsumerKey { get; set; } 15 | public string ConsumerSecret { get; set; } 16 | public string AccessToken { get; set; } 17 | public string AccessSecret { get; set; } 18 | } 19 | 20 | public class TinyTwitter 21 | { 22 | private readonly OAuthInfo oauth; 23 | 24 | public TinyTwitter(OAuthInfo oauth) 25 | { 26 | this.oauth = oauth; 27 | } 28 | 29 | public void UpdateStatus(string message) 30 | { 31 | new RequestBuilder(oauth, "POST", "https://api.twitter.com/1.1/statuses/update.json") 32 | .AddParameter("status", message) 33 | .Execute(); 34 | } 35 | 36 | #region RequestBuilder 37 | 38 | private class RequestBuilder 39 | { 40 | private const string VERSION = "1.0"; 41 | private const string SIGNATURE_METHOD = "HMAC-SHA1"; 42 | 43 | private readonly OAuthInfo oauth; 44 | private readonly string method; 45 | private readonly IDictionary customParameters; 46 | private readonly string url; 47 | 48 | public RequestBuilder(OAuthInfo oauth, string method, string url) 49 | { 50 | this.oauth = oauth; 51 | this.method = method; 52 | this.url = url; 53 | customParameters = new Dictionary(); 54 | } 55 | 56 | public RequestBuilder AddParameter(string name, string value) 57 | { 58 | customParameters.Add(name, value.EncodeRFC3986()); 59 | return this; 60 | } 61 | 62 | public string Execute() 63 | { 64 | var timespan = GetTimestamp(); 65 | var nonce = CreateNonce(); 66 | 67 | var parameters = new Dictionary(customParameters); 68 | AddOAuthParameters(parameters, timespan, nonce); 69 | 70 | var signature = GenerateSignature(parameters); 71 | var headerValue = GenerateAuthorizationHeaderValue(parameters, signature); 72 | 73 | var request = (HttpWebRequest)WebRequest.Create(GetRequestUrl()); 74 | request.Method = method; 75 | request.ContentType = "application/x-www-form-urlencoded"; 76 | 77 | request.Headers.Add("Authorization", headerValue); 78 | 79 | WriteRequestBody(request); 80 | 81 | // It looks like a bug in HttpWebRequest. It throws random TimeoutExceptions 82 | // after some requests. Abort the request seems to work. More info: 83 | // http://stackoverflow.com/questions/2252762/getrequeststream-throws-timeout-exception-randomly 84 | 85 | var response = request.GetResponse(); 86 | 87 | string content; 88 | 89 | using (var stream = response.GetResponseStream()) 90 | { 91 | using var reader = new StreamReader(stream); 92 | content = reader.ReadToEnd(); 93 | } 94 | 95 | request.Abort(); 96 | 97 | return content; 98 | } 99 | 100 | private void WriteRequestBody(HttpWebRequest request) 101 | { 102 | if (method == "GET") 103 | return; 104 | 105 | var requestBody = Encoding.ASCII.GetBytes(GetCustomParametersString()); 106 | using var stream = request.GetRequestStream(); 107 | stream.Write(requestBody, 0, requestBody.Length); 108 | } 109 | 110 | private string GetRequestUrl() 111 | { 112 | if (method != "GET" || customParameters.Count == 0) 113 | return url; 114 | 115 | return string.Format("{0}?{1}", url, GetCustomParametersString()); 116 | } 117 | 118 | private string GetCustomParametersString() 119 | { 120 | return customParameters.Select(x => string.Format("{0}={1}", x.Key, x.Value)).Join("&"); 121 | } 122 | 123 | private static string GenerateAuthorizationHeaderValue(IEnumerable> parameters, string signature) 124 | { 125 | return new StringBuilder("OAuth ") 126 | .Append(parameters.Concat(new KeyValuePair("oauth_signature", signature)) 127 | .Where(x => x.Key.StartsWith("oauth_")) 128 | .Select(x => string.Format("{0}=\"{1}\"", x.Key, x.Value.EncodeRFC3986())) 129 | .Join(",")) 130 | .ToString(); 131 | } 132 | 133 | private string GenerateSignature(IEnumerable> parameters) 134 | { 135 | var dataToSign = new StringBuilder() 136 | .Append(method).Append("&") 137 | .Append(url.EncodeRFC3986()).Append("&") 138 | .Append(parameters 139 | .OrderBy(x => x.Key) 140 | .Select(x => string.Format("{0}={1}", x.Key, x.Value)) 141 | .Join("&") 142 | .EncodeRFC3986()); 143 | 144 | var signatureKey = string.Format("{0}&{1}", oauth.ConsumerSecret.EncodeRFC3986(), oauth.AccessSecret.EncodeRFC3986()); 145 | var sha1 = new HMACSHA1(Encoding.ASCII.GetBytes(signatureKey)); 146 | 147 | var signatureBytes = sha1.ComputeHash(Encoding.ASCII.GetBytes(dataToSign.ToString())); 148 | return Convert.ToBase64String(signatureBytes); 149 | } 150 | 151 | private void AddOAuthParameters(IDictionary parameters, string timestamp, string nonce) 152 | { 153 | parameters.Add("oauth_version", VERSION); 154 | parameters.Add("oauth_consumer_key", oauth.ConsumerKey); 155 | parameters.Add("oauth_nonce", nonce); 156 | parameters.Add("oauth_signature_method", SIGNATURE_METHOD); 157 | parameters.Add("oauth_timestamp", timestamp); 158 | parameters.Add("oauth_token", oauth.AccessToken); 159 | } 160 | 161 | private static string GetTimestamp() 162 | { 163 | return ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString(); 164 | } 165 | 166 | private static string CreateNonce() 167 | { 168 | return new Random().Next(0x0000000, 0x7fffffff).ToString("X8"); 169 | } 170 | } 171 | 172 | #endregion 173 | } 174 | 175 | public static class TinyTwitterHelperExtensions 176 | { 177 | public static string Join(this IEnumerable items, string separator) 178 | { 179 | return string.Join(separator, items.ToArray()); 180 | } 181 | 182 | public static IEnumerable Concat(this IEnumerable items, T value) 183 | { 184 | return items.Concat(new[] { value }); 185 | } 186 | 187 | public static string EncodeRFC3986(this string value) 188 | { 189 | // From Twitterizer http://www.twitterizer.net/ 190 | 191 | if (string.IsNullOrEmpty(value)) 192 | return string.Empty; 193 | 194 | var encoded = Uri.EscapeDataString(value); 195 | 196 | return Regex 197 | .Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper()) 198 | .Replace("(", "%28") 199 | .Replace(")", "%29") 200 | .Replace("$", "%24") 201 | .Replace("!", "%21") 202 | .Replace("*", "%2A") 203 | .Replace("'", "%27") 204 | .Replace("%7E", "~"); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /settings.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "SteamUsername": "", 3 | "SteamPassword": "", 4 | "Twitter": 5 | { 6 | "ConsumerKey": "", 7 | "ConsumerSecret": "", 8 | "AccessToken": "", 9 | "AccessSecret": "" 10 | } 11 | } 12 | --------------------------------------------------------------------------------