diff --git a/RedditRandomNumberGiveawayHelper.sln b/RedditRandomNumberGiveawayHelper.sln new file mode 100644 index 0000000..ee48e5a --- /dev/null +++ b/RedditRandomNumberGiveawayHelper.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.21005.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedditRandomNumberGiveawayHelper", "RedditRandomNumberGiveawayHelper\RedditRandomNumberGiveawayHelper.csproj", "{8203F99F-1FF9-40D3-953E-4B8F28A91932}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedditSharp", "RedditSharp\RedditSharp.csproj", "{A368CB75-75F0-4489-904D-B5CEBB0FE624}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8203F99F-1FF9-40D3-953E-4B8F28A91932}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8203F99F-1FF9-40D3-953E-4B8F28A91932}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8203F99F-1FF9-40D3-953E-4B8F28A91932}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8203F99F-1FF9-40D3-953E-4B8F28A91932}.Release|Any CPU.Build.0 = Release|Any CPU + {A368CB75-75F0-4489-904D-B5CEBB0FE624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A368CB75-75F0-4489-904D-B5CEBB0FE624}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A368CB75-75F0-4489-904D-B5CEBB0FE624}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A368CB75-75F0-4489-904D-B5CEBB0FE624}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/RedditRandomNumberGiveawayHelper/App.config b/RedditRandomNumberGiveawayHelper/App.config new file mode 100644 index 0000000..9c05822 --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/RedditRandomNumberGiveawayHelper/MainForm.Designer.cs b/RedditRandomNumberGiveawayHelper/MainForm.Designer.cs new file mode 100644 index 0000000..afe3ebe --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/MainForm.Designer.cs @@ -0,0 +1,147 @@ +namespace RedditRandomNumberGiveawayHelper +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.label1 = new System.Windows.Forms.Label(); + this.textBox1 = new System.Windows.Forms.TextBox(); + this.numericUpDown1 = new System.Windows.Forms.NumericUpDown(); + this.label2 = new System.Windows.Forms.Label(); + this.button1 = new System.Windows.Forms.Button(); + this.textBox2 = new System.Windows.Forms.TextBox(); + this.label3 = new System.Windows.Forms.Label(); + ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit(); + this.SuspendLayout(); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 15); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(100, 13); + this.label1.TabIndex = 0; + this.label1.Text = "Giveaway Post URI"; + // + // textBox1 + // + this.textBox1.Location = new System.Drawing.Point(118, 12); + this.textBox1.Name = "textBox1"; + this.textBox1.Size = new System.Drawing.Size(463, 20); + this.textBox1.TabIndex = 1; + // + // numericUpDown1 + // + this.numericUpDown1.Location = new System.Drawing.Point(118, 39); + this.numericUpDown1.Maximum = new decimal(new int[] { + 100000, + 0, + 0, + 0}); + this.numericUpDown1.Minimum = new decimal(new int[] { + 1, + 0, + 0, + 0}); + this.numericUpDown1.Name = "numericUpDown1"; + this.numericUpDown1.Size = new System.Drawing.Size(115, 20); + this.numericUpDown1.TabIndex = 2; + this.numericUpDown1.Value = new decimal(new int[] { + 5000, + 0, + 0, + 0}); + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(12, 41); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(57, 13); + this.label2.TabIndex = 3; + this.label2.Text = "Max Value"; + // + // button1 + // + this.button1.Location = new System.Drawing.Point(239, 38); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(342, 23); + this.button1.TabIndex = 4; + this.button1.Text = "Get Me A Random Winner"; + this.button1.UseVisualStyleBackColor = true; + this.button1.Click += new System.EventHandler(this.button1_Click); + // + // textBox2 + // + this.textBox2.Location = new System.Drawing.Point(13, 95); + this.textBox2.Multiline = true; + this.textBox2.Name = "textBox2"; + this.textBox2.ReadOnly = true; + this.textBox2.Size = new System.Drawing.Size(568, 140); + this.textBox2.TabIndex = 5; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(268, 79); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(37, 13); + this.label3.TabIndex = 6; + this.label3.Text = "Result"; + // + // Form1 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(593, 247); + this.Controls.Add(this.label3); + this.Controls.Add(this.textBox2); + this.Controls.Add(this.button1); + this.Controls.Add(this.label2); + this.Controls.Add(this.numericUpDown1); + this.Controls.Add(this.textBox1); + this.Controls.Add(this.label1); + this.Name = "Form1"; + this.Text = "Form1"; + ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label label1; + private System.Windows.Forms.TextBox textBox1; + private System.Windows.Forms.NumericUpDown numericUpDown1; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Button button1; + private System.Windows.Forms.TextBox textBox2; + private System.Windows.Forms.Label label3; + } +} + diff --git a/RedditRandomNumberGiveawayHelper/MainForm.cs b/RedditRandomNumberGiveawayHelper/MainForm.cs new file mode 100644 index 0000000..e21f50e --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/MainForm.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using RedditSharp; +using RedditSharp.Things; +using System.Net; +using System.IO; +using System.Text.RegularExpressions; + +namespace RedditRandomNumberGiveawayHelper +{ + public partial class MainForm : Form + { + //make sure to format with max number + private static string RANDOM_ORG_URI = "https://www.random.org/integers/?num=1&min=1&max={0}&col=1&base=10&format=plain&rnd=new"; + + public MainForm() + { + InitializeComponent(); + } + + private void button1_Click(object sender, EventArgs e) + { + //textBox2.Text += string.Format("{0}", Environment.NewLine); + var reddit = new Reddit(); + Post giveawayPost = null; + int? randomNumber = null; + + Regex numberPost = new Regex(@"(\d{1,6})"); + + try + { + textBox2.Text += string.Format("Getting giveaway post...{0}", Environment.NewLine); + giveawayPost = reddit.GetPost(new Uri(textBox1.Text)); + } + catch (Exception ex) + { + textBox2.Text += string.Format("{1}{0}{2}{0}", + Environment.NewLine, + "Failed getting giveaway post", + "You sure that's the right URI (alsomake sure to get the full uri from the address bar)"); + return; + } + + textBox2.Text += string.Format("{1}{2}{0}{3}{4}{0}", + Environment.NewLine, + "Post title: ", + giveawayPost.Title, + "Comment count: ", + giveawayPost.CommentCount); + + try + { + textBox2.Text += string.Format("{1}{0}", + Environment.NewLine, + "Getting random number from random.org..."); + WebRequest randomDotOrgRequest = WebRequest.Create(string.Format(RANDOM_ORG_URI, decimal.Round(numericUpDown1.Value, 0))); + using (WebResponse resp = randomDotOrgRequest.GetResponse()) + { + using (StreamReader sr = new StreamReader(resp.GetResponseStream())) + { + randomNumber = int.Parse(sr.ReadToEnd()); + } + } + } + catch (Exception ex) + { + textBox2.Text += string.Format("{1}{0}{2}{0}", + Environment.NewLine, + "Failed getting giveaway post", + ex); + return; + } + + textBox2.Text += string.Format("{1}{2}{0}{3}{4}{0}", + Environment.NewLine, + "Random Number: ", + randomNumber, + "Getting winning comment...", + "This might take a while..."); + + Dictionary nums = giveawayPost.Comments.Where(c => c.Body != null && numberPost.IsMatch(c.Body)).ToDictionary(k => k.Shortlink, elementSelector: x => int.Parse(numberPost.Match(x.Body).Captures[0].Value)); + string winningNumKey = null; + int? winningNumVal = null; + int? diff = null; + + for (int i = 0; (winningNumKey == null && winningNumVal == null) && randomNumber + i < decimal.Round(numericUpDown1.Value, 0); i++) + { + foreach (var x in nums) + { + if (x.Value == randomNumber + i || x.Value == randomNumber - i) + { + winningNumKey = x.Key; + winningNumVal = x.Value; + diff = i; + break; + } + } + } + + Comment winningComment = giveawayPost.Comments.FirstOrDefault(w => w.Shortlink == winningNumKey); + + textBox2.Text += string.Format("{1}{2}{0}{3}{4}{0}{5}{6}{0}{7}{8}{0}", + Environment.NewLine, + "Winning comment (link): ", + winningNumKey, + "Winning comment (body): ", + winningComment.Body, + "Winning comment (commenter): ", + winningComment.Author, + "Diff: ", + diff + ); + } + } +} diff --git a/RedditRandomNumberGiveawayHelper/MainForm.resx b/RedditRandomNumberGiveawayHelper/MainForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/MainForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/RedditRandomNumberGiveawayHelper/Program.cs b/RedditRandomNumberGiveawayHelper/Program.cs new file mode 100644 index 0000000..bb0522d --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace RedditRandomNumberGiveawayHelper +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainForm()); + } + } +} diff --git a/RedditRandomNumberGiveawayHelper/Properties/AssemblyInfo.cs b/RedditRandomNumberGiveawayHelper/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..56166f8 --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RedditRandomNumberGiveawayHelper")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RedditRandomNumberGiveawayHelper")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("30ddc09b-6ce1-4518-b330-67e36066116e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/RedditRandomNumberGiveawayHelper/Properties/Resources.Designer.cs b/RedditRandomNumberGiveawayHelper/Properties/Resources.Designer.cs new file mode 100644 index 0000000..bd87c8d --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34209 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace RedditRandomNumberGiveawayHelper.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("RedditRandomNumberGiveawayHelper.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/RedditRandomNumberGiveawayHelper/Properties/Resources.resx b/RedditRandomNumberGiveawayHelper/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/RedditRandomNumberGiveawayHelper/Properties/Settings.Designer.cs b/RedditRandomNumberGiveawayHelper/Properties/Settings.Designer.cs new file mode 100644 index 0000000..f9ad36f --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34209 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace RedditRandomNumberGiveawayHelper.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/RedditRandomNumberGiveawayHelper/Properties/Settings.settings b/RedditRandomNumberGiveawayHelper/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/RedditRandomNumberGiveawayHelper/RedditRandomNumberGiveawayHelper.csproj b/RedditRandomNumberGiveawayHelper/RedditRandomNumberGiveawayHelper.csproj new file mode 100644 index 0000000..cb9c494 --- /dev/null +++ b/RedditRandomNumberGiveawayHelper/RedditRandomNumberGiveawayHelper.csproj @@ -0,0 +1,96 @@ + + + + + Debug + AnyCPU + {8203F99F-1FF9-40D3-953E-4B8F28A91932} + WinExe + Properties + RedditRandomNumberGiveawayHelper + RedditRandomNumberGiveawayHelper + v4.5.1 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + Form + + + MainForm.cs + + + + + MainForm.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + + {a368cb75-75f0-4489-904d-b5cebb0fe624} + RedditSharp + + + + + \ No newline at end of file diff --git a/RedditSharp/AuthProvider.cs b/RedditSharp/AuthProvider.cs new file mode 100644 index 0000000..3993687 --- /dev/null +++ b/RedditSharp/AuthProvider.cs @@ -0,0 +1,174 @@ +using System; +using System.Net; +using System.Security.Authentication; +using System.Text; +using Newtonsoft.Json.Linq; +using RedditSharp.Things; + +namespace RedditSharp +{ + public class AuthProvider + { + private const string AccessUrl = "https://ssl.reddit.com/api/v1/access_token"; + private const string OauthGetMeUrl = "https://oauth.reddit.com/api/v1/me"; + + public static string OAuthToken { get; set; } + public static string RefreshToken { get; set; } + + [Flags] + public enum Scope + { + none = 0x0, + identity = 0x1, + edit = 0x2, + flair = 0x4, + history = 0x8, + modconfig = 0x10, + modflair = 0x20, + modlog = 0x40, + modposts = 0x80, + modwiki = 0x100, + mysubreddits = 0x200, + privatemessages = 0x400, + read = 0x800, + report = 0x1000, + save = 0x2000, + submit = 0x4000, + subscribe = 0x8000, + vote = 0x10000, + wikiedit = 0x20000, + wikiread = 0x40000 + } + private readonly IWebAgent _webAgent; + private readonly string _redirectUri; + private readonly string _clientId; + private readonly string _clientSecret; + + /// + /// Allows use of reddit's OAuth interface, using an app set up at https://ssl.reddit.com/prefs/apps/. + /// + /// Granted by reddit as part of app. + /// Granted by reddit as part of app. + /// Selected as part of app. Reddit will send users back here. + public AuthProvider(string clientId, string clientSecret, string redirectUri) + { + _clientId = clientId; + _clientSecret = clientSecret; + _redirectUri = redirectUri; + _webAgent = new WebAgent(); + } + + /// + /// Creates the reddit OAuth2 Url to redirect the user to for authorization. + /// + /// Used to verify that the user received is the user that was sent + /// Determines what actions can be performed against the user. + /// Set to true for access lasting longer than one hour. + /// + public string GetAuthUrl(string state, Scope scope, bool permanent = false) + { + return String.Format("https://ssl.reddit.com/api/v1/authorize?client_id={0}&response_type=code&state={1}&redirect_uri={2}&duration={3}&scope={4}", _clientId, state, _redirectUri, permanent ? "permanent" : "temporary", scope.ToString().Replace(" ","")); + } + + /// + /// Gets the OAuth token for the user associated with the provided code. + /// + /// Sent by reddit as a parameter in the return uri. + /// Set to true for refresh requests. + /// + public string GetOAuthToken(string code, bool isRefresh = false) + { + if (Type.GetType("Mono.Runtime") != null) + ServicePointManager.ServerCertificateValidationCallback = (s, c, ch, ssl) => true; + _webAgent.Cookies = new CookieContainer(); + + var request = _webAgent.CreatePost(AccessUrl); + + request.Headers["Authorization"] = "Basic " + Convert.ToBase64String(Encoding.Default.GetBytes(_clientId + ":" + _clientSecret)); + var stream = request.GetRequestStream(); + + if (isRefresh) + { + _webAgent.WritePostBody(stream, new + { + grant_type = "refresh_token", + refresh_token = code + }); + } + else + { + _webAgent.WritePostBody(stream, new + { + grant_type = "authorization_code", + code, + redirect_uri = _redirectUri + }); + } + + stream.Close(); + var json = _webAgent.ExecuteRequest(request); + if (json["access_token"] != null) + { + if (json["refresh_token"] != null) + RefreshToken = json["refresh_token"].ToString(); + OAuthToken = json["access_token"].ToString(); + return json["access_token"].ToString(); + } + throw new AuthenticationException("Could not log in."); + } + + /// + /// Gets the OAuth token for the user. + /// + /// The username. + /// The user's password. + /// The access token + public string GetOAuthToken(string username, string password) + { + if (Type.GetType("Mono.Runtime") != null) + ServicePointManager.ServerCertificateValidationCallback = (s, c, ch, ssl) => true; + _webAgent.Cookies = new CookieContainer(); + + var request = _webAgent.CreatePost(AccessUrl); + + request.Headers["Authorization"] = "Basic " + Convert.ToBase64String(Encoding.Default.GetBytes(_clientId + ":" + _clientSecret)); + var stream = request.GetRequestStream(); + + _webAgent.WritePostBody(stream, new + { + grant_type = "password", + username, + password, + redirect_uri = _redirectUri + }); + + stream.Close(); + var json = _webAgent.ExecuteRequest(request); + if (json["access_token"] != null) + { + if (json["refresh_token"] != null) + RefreshToken = json["refresh_token"].ToString(); + OAuthToken = json["access_token"].ToString(); + return json["access_token"].ToString(); + } + throw new AuthenticationException("Could not log in."); + } + + /// + /// Gets a user authenticated by OAuth2. + /// + /// Obtained using GetOAuthToken + /// + [Obsolete("Reddit.InitOrUpdateUser is preferred")] + public AuthenticatedUser GetUser(string accessToken) + { + var request = _webAgent.CreateGet(OauthGetMeUrl); + request.Headers["Authorization"] = String.Format("bearer {0}", accessToken); + var response = (HttpWebResponse)request.GetResponse(); + var result = _webAgent.GetResponseString(response.GetResponseStream()); + var thingjson = "{\"kind\": \"t2\", \"data\": " + result + "}"; + var json = JObject.Parse(thingjson); + return new AuthenticatedUser().Init(new Reddit(), json, _webAgent); + } + } +} diff --git a/RedditSharp/Captcha.cs b/RedditSharp/Captcha.cs new file mode 100644 index 0000000..aa3e6f2 --- /dev/null +++ b/RedditSharp/Captcha.cs @@ -0,0 +1,18 @@ +using System; + +namespace RedditSharp +{ + public struct Captcha + { + private const string UrlFormat = "http://www.reddit.com/captcha/{0}"; + + public readonly string Id; + public readonly Uri Url; + + internal Captcha(string id) + { + Id = id; + Url = new Uri(string.Format(UrlFormat, Id), UriKind.Absolute); + } + } +} diff --git a/RedditSharp/CaptchaFailedException.cs b/RedditSharp/CaptchaFailedException.cs new file mode 100644 index 0000000..0c86589 --- /dev/null +++ b/RedditSharp/CaptchaFailedException.cs @@ -0,0 +1,24 @@ +using System; + +namespace RedditSharp +{ + public class CaptchaFailedException : RedditException + { + public CaptchaFailedException() + { + + } + + public CaptchaFailedException(string message) + : base(message) + { + + } + + public CaptchaFailedException(string message, Exception inner) + : base(message, inner) + { + + } + } +} diff --git a/RedditSharp/CaptchaResponse.cs b/RedditSharp/CaptchaResponse.cs new file mode 100644 index 0000000..90f3fa1 --- /dev/null +++ b/RedditSharp/CaptchaResponse.cs @@ -0,0 +1,14 @@ +namespace RedditSharp +{ + public class CaptchaResponse + { + public readonly string Answer; + + public bool Cancel { get { return string.IsNullOrEmpty(Answer); } } + + public CaptchaResponse(string answer = null) + { + Answer = answer; + } + } +} diff --git a/RedditSharp/ConsoleCaptchaSolver.cs b/RedditSharp/ConsoleCaptchaSolver.cs new file mode 100644 index 0000000..0943211 --- /dev/null +++ b/RedditSharp/ConsoleCaptchaSolver.cs @@ -0,0 +1,17 @@ +using System; + +namespace RedditSharp +{ + public class ConsoleCaptchaSolver : ICaptchaSolver + { + public CaptchaResponse HandleCaptcha(Captcha captcha) + { + Console.WriteLine("Captcha required! The captcha ID is {0}", captcha.Id); + Console.WriteLine("You can find the captcha image at this url: {0}", captcha.Url); + Console.WriteLine("Please input your captcha response or empty string to cancel:"); + var response = Console.ReadLine(); + CaptchaResponse captchaResponse = new CaptchaResponse(string.IsNullOrEmpty(response) ? null : response); + return captchaResponse; + } + } +} diff --git a/RedditSharp/Domain.cs b/RedditSharp/Domain.cs new file mode 100644 index 0000000..3daf30b --- /dev/null +++ b/RedditSharp/Domain.cs @@ -0,0 +1,61 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RedditSharp.Things; + +namespace RedditSharp +{ + public class Domain + { + private const string DomainPostUrl = "/domain/{0}.json"; + private const string DomainNewUrl = "/domain/{0}/new.json?sort=new"; + private const string DomainHotUrl = "/domain/{0}/hot.json"; + private const string FrontPageUrl = "/.json"; + + [JsonIgnore] + private Reddit Reddit { get; set; } + + [JsonIgnore] + private IWebAgent WebAgent { get; set; } + + [JsonIgnore] + public string Name { get; set; } + + public Listing Posts + { + get + { + return new Listing(Reddit, string.Format(DomainPostUrl, Name), WebAgent); + } + } + + public Listing New + { + get + { + return new Listing(Reddit, string.Format(DomainNewUrl, Name), WebAgent); + } + } + + public Listing Hot + { + get + { + return new Listing(Reddit, string.Format(DomainHotUrl, Name), WebAgent); + } + } + + protected internal Domain(Reddit reddit, Uri domain, IWebAgent webAgent) + { + Reddit = reddit; + WebAgent = webAgent; + Name = domain.Host; + } + + public override string ToString() + { + return "/domain/" + Name; + } + } +} + diff --git a/RedditSharp/DuplicateLinkException.cs b/RedditSharp/DuplicateLinkException.cs new file mode 100644 index 0000000..b5b90a0 --- /dev/null +++ b/RedditSharp/DuplicateLinkException.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace RedditSharp +{ + /// + /// Exception that gets thrown if you try and submit a duplicate link to a SubReddit + /// + public class DuplicateLinkException : RedditException + { + public DuplicateLinkException() + { + } + + public DuplicateLinkException(string message) + : base(message) + { + } + + public DuplicateLinkException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/RedditSharp/Extensions.cs b/RedditSharp/Extensions.cs new file mode 100644 index 0000000..72d3e1a --- /dev/null +++ b/RedditSharp/Extensions.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace RedditSharp +{ + public static class Extensions + { + public static T ValueOrDefault(this IEnumerable enumerable) + { + if (enumerable == null) + return default(T); + return enumerable.Value(); + } + } +} diff --git a/RedditSharp/FlairTemplate.cs b/RedditSharp/FlairTemplate.cs new file mode 100644 index 0000000..34d37c8 --- /dev/null +++ b/RedditSharp/FlairTemplate.cs @@ -0,0 +1,8 @@ +namespace RedditSharp +{ + public class UserFlairTemplate // TODO: Consider using this class to set templates as well + { + public string Text { get; set; } + public string CssClass { get; set; } + } +} diff --git a/RedditSharp/FlairType.cs b/RedditSharp/FlairType.cs new file mode 100644 index 0000000..77bbb12 --- /dev/null +++ b/RedditSharp/FlairType.cs @@ -0,0 +1,8 @@ +namespace RedditSharp +{ + public enum FlairType + { + Link, + User + } +} diff --git a/RedditSharp/ICaptchaSolver.cs b/RedditSharp/ICaptchaSolver.cs new file mode 100644 index 0000000..f838643 --- /dev/null +++ b/RedditSharp/ICaptchaSolver.cs @@ -0,0 +1,7 @@ +namespace RedditSharp +{ + public interface ICaptchaSolver + { + CaptchaResponse HandleCaptcha(Captcha captcha); + } +} diff --git a/RedditSharp/IWebAgent.cs b/RedditSharp/IWebAgent.cs new file mode 100644 index 0000000..a107500 --- /dev/null +++ b/RedditSharp/IWebAgent.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Net; +using Newtonsoft.Json.Linq; + +namespace RedditSharp +{ + public interface IWebAgent + { + CookieContainer Cookies { get; set; } + string AuthCookie { get; set; } + string AccessToken { get; set; } + HttpWebRequest CreateRequest(string url, string method); + HttpWebRequest CreateGet(string url); + HttpWebRequest CreatePost(string url); + string GetResponseString(Stream stream); + void WritePostBody(Stream stream, object data, params string[] additionalFields); + JToken CreateAndExecuteRequest(string url); + JToken ExecuteRequest(HttpWebRequest request); + } +} diff --git a/RedditSharp/LinkData.cs b/RedditSharp/LinkData.cs new file mode 100644 index 0000000..c86e9f7 --- /dev/null +++ b/RedditSharp/LinkData.cs @@ -0,0 +1,17 @@ +namespace RedditSharp +{ + internal class LinkData : SubmitData + { + [RedditAPIName("extension")] + internal string Extension { get; set; } + + [RedditAPIName("url")] + internal string URL { get; set; } + + internal LinkData() + { + Extension = "json"; + Kind = "link"; + } + } +} diff --git a/RedditSharp/Listing.cs b/RedditSharp/Listing.cs new file mode 100644 index 0000000..800a5db --- /dev/null +++ b/RedditSharp/Listing.cs @@ -0,0 +1,248 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using RedditSharp.Things; + +namespace RedditSharp +{ + public class Listing : IEnumerable where T : Thing + { + /// + /// Gets the default number of listings returned per request + /// + internal const int DefaultListingPerRequest = 25; + + private IWebAgent WebAgent { get; set; } + private Reddit Reddit { get; set; } + private string Url { get; set; } + + /// + /// Creates a new Listing instance + /// + /// + /// + /// + internal Listing(Reddit reddit, string url, IWebAgent webAgent) + { + WebAgent = webAgent; + Reddit = reddit; + Url = url; + } + + /// + /// Returns an enumerator that iterates through a collection, using the specified number of listings per + /// request and optionally the maximum number of listings + /// + /// The number of listings to be returned per request + /// The maximum number of listings to return + /// + public IEnumerator GetEnumerator(int limitPerRequest, int maximumLimit = -1) + { + return new ListingEnumerator(this, limitPerRequest, maximumLimit); + } + + /// + /// Returns an enumerator that iterates through a collection, using the default number of listings per request + /// + /// + public IEnumerator GetEnumerator() + { + return GetEnumerator(DefaultListingPerRequest); + } + + /// + /// Returns an enumerator that iterates through a collection + /// + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Returns an IEnumerable instance which will return the specified maximum number of listings + /// + /// + /// + public IEnumerable GetListing(int maximumLimit) + { + return GetListing(maximumLimit, DefaultListingPerRequest); + } + + /// + /// Returns an IEnumerable instance which will return the specified maximum number of listings + /// with the limited number per request + /// + /// + /// + /// + public IEnumerable GetListing(int maximumLimit, int limitPerRequest) + { + // Get the enumerator with the specified maximum and per request limits + var enumerator = GetEnumerator(limitPerRequest, maximumLimit); + + return GetEnumerator(enumerator); + } + + /// + /// Converts an IEnumerator instance to an IEnumerable + /// + /// + /// + private static IEnumerable GetEnumerator(IEnumerator enumerator) + { + while (enumerator.MoveNext()) + { + yield return enumerator.Current; + } + } + +#pragma warning disable 0693 + private class ListingEnumerator : IEnumerator where T : Thing + { + private Listing Listing { get; set; } + private int CurrentPageIndex { get; set; } + private string After { get; set; } + private string Before { get; set; } + private Thing[] CurrentPage { get; set; } + private int Count { get; set; } + private int LimitPerRequest { get; set; } + private int MaximumLimit { get; set; } + + /// + /// Creates a new ListingEnumerator instance + /// + /// + /// The number of listings to be returned per request. -1 will exclude this parameter and use the Reddit default (25) + /// The maximum number of listings to return, -1 will not add a limit + public ListingEnumerator(Listing listing, int limitPerRequest, int maximumLimit) + { + Listing = listing; + CurrentPageIndex = -1; + CurrentPage = new Thing[0]; + + // Set the listings per page (if not specified, use the Reddit default of 25) and the maximum listings + LimitPerRequest = (limitPerRequest <= 0 ? DefaultListingPerRequest : limitPerRequest); + MaximumLimit = maximumLimit; + } + + public T Current + { + get + { + return (T)CurrentPage[CurrentPageIndex]; + } + } + + private void FetchNextPage() + { + var url = Listing.Url; + + if (After != null) + { + url += (url.Contains("?") ? "&" : "?") + "after=" + After; + } + + if (LimitPerRequest != -1) + { + int limit = LimitPerRequest; + + if (limit > MaximumLimit) + { + // If the limit is more than the maximum number of listings, adjust + limit = MaximumLimit; + } + else if (Count + limit > MaximumLimit) + { + // If a smaller subset of listings are needed, adjust the limit + limit = MaximumLimit - Count; + } + + if (limit > 0) + { + // Add the limit, the maximum number of items to be returned per page + url += (url.Contains("?") ? "&" : "?") + "limit=" + limit; + } + } + + if (Count > 0) + { + // Add the count, the number of items already seen in this listing + // The Reddit API uses this to determine when to give values for before and after fields + url += (url.Contains("?") ? "&" : "?") + "count=" + Count; + } + + var request = Listing.WebAgent.CreateGet(url); + var response = request.GetResponse(); + var data = Listing.WebAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + if (json["kind"].ValueOrDefault() != "Listing") + throw new FormatException("Reddit responded with an object that is not a listing."); + Parse(json); + } + + private void Parse(JToken json) + { + var children = json["data"]["children"] as JArray; + CurrentPage = new Thing[children.Count]; + + for (int i = 0; i < CurrentPage.Length; i++) + CurrentPage[i] = Thing.Parse(Listing.Reddit, children[i], Listing.WebAgent); + + // Increase the total count of items returned + Count += CurrentPage.Length; + + After = json["data"]["after"].Value(); + Before = json["data"]["before"].Value(); + } + + public void Dispose() + { + // ... + } + + object System.Collections.IEnumerator.Current + { + get { return Current; } + } + + public bool MoveNext() + { + CurrentPageIndex++; + if (CurrentPageIndex == CurrentPage.Length) + { + if (After == null && CurrentPageIndex != 0) + { + // No more pages to return + return false; + } + + if (MaximumLimit != -1 && Count >= MaximumLimit) + { + // Maximum listing count returned + return false; + } + + // Get the next page + FetchNextPage(); + CurrentPageIndex = 0; + + if (CurrentPage.Length == 0) + { + // No listings were returned in the page + return false; + } + } + return true; + } + + public void Reset() + { + After = Before = null; + CurrentPageIndex = -1; + CurrentPage = new Thing[0]; + } + } +#pragma warning restore + } +} diff --git a/RedditSharp/ModeratorPermission.cs b/RedditSharp/ModeratorPermission.cs new file mode 100644 index 0000000..4549f5b --- /dev/null +++ b/RedditSharp/ModeratorPermission.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp +{ + [Flags] + public enum ModeratorPermission + { + None = 0x00, + Access = 0x01, + Config = 0x02, + Flair = 0x04, + Mail = 0x08, + Posts = 0x10, + Wiki = 0x20, + All = Access | Config | Flair | Mail | Posts | Wiki + } + + internal class ModeratorPermissionConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var data = String.Join(",", JArray.Load(reader).Select(t => t.ToString())); + + ModeratorPermission result; + + var valid = Enum.TryParse(data, true, out result); + + if (!valid) + result = ModeratorPermission.None; + + return result; + } + + public override bool CanConvert(Type objectType) + { + // NOTE: Not sure if this is what is supposed to be returned + // This method wasn't called in my (Sharparam) tests so unsure what it does + return objectType == typeof (ModeratorPermission); + } + } +} diff --git a/RedditSharp/ModeratorUser.cs b/RedditSharp/ModeratorUser.cs new file mode 100644 index 0000000..92a3404 --- /dev/null +++ b/RedditSharp/ModeratorUser.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp +{ + public class ModeratorUser + { + public ModeratorUser(Reddit reddit, JToken json) + { + JsonConvert.PopulateObject(json.ToString(), this, reddit.JsonSerializerSettings); + } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("mod_permissions")] + [JsonConverter(typeof (ModeratorPermissionConverter))] + public ModeratorPermission Permissions { get; set; } + + public override string ToString() + { + return Name; + } + } +} diff --git a/RedditSharp/MultipartFormBuilder.cs b/RedditSharp/MultipartFormBuilder.cs new file mode 100644 index 0000000..49fcce7 --- /dev/null +++ b/RedditSharp/MultipartFormBuilder.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.Net; + +namespace RedditSharp +{ + public class MultipartFormBuilder + { + public HttpWebRequest Request { get; set; } + + private string Boundary { get; set; } + private MemoryStream Buffer { get; set; } + private TextWriter TextBuffer { get; set; } + + public MultipartFormBuilder(HttpWebRequest request) + { + // TODO: See about regenerating the boundary when needed + Request = request; + var random = new Random(); + Boundary = "----------" + CreateRandomBoundary(); + request.ContentType = "multipart/form-data; boundary=" + Boundary; + Buffer = new MemoryStream(); + TextBuffer = new StreamWriter(Buffer); + } + + private string CreateRandomBoundary() + { + // TODO: There's probably a better way to go about this + const string characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + string value = ""; + var random = new Random(); + for (int i = 0; i < 10; i++) + value += characters[random.Next(characters.Length)]; + return value; + } + + public void AddDynamic(object data) + { + var type = data.GetType(); + var properties = type.GetProperties(); + foreach (var property in properties) + { + var entry = Convert.ToString(property.GetValue(data, null)); + AddString(property.Name, entry); + } + } + + public void AddString(string name, string value) + { + TextBuffer.Write("{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n", + "--" + Boundary, name, value); + TextBuffer.Flush(); + } + + public void AddFile(string name, string filename, byte[] value, string contentType) + { + TextBuffer.Write("{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n", + "--" + Boundary, name, filename, contentType); + TextBuffer.Flush(); + Buffer.Write(value, 0, value.Length); + Buffer.Flush(); + TextBuffer.Write("\r\n"); + TextBuffer.Flush(); + } + + public void Finish() + { + TextBuffer.Write("--" + Boundary + "--"); + TextBuffer.Flush(); + var stream = Request.GetRequestStream(); + Buffer.Seek(0, SeekOrigin.Begin); + Buffer.WriteTo(stream); + stream.Close(); + } + } +} diff --git a/RedditSharp/Properties/AssemblyInfo.cs b/RedditSharp/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..09631d2 --- /dev/null +++ b/RedditSharp/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RedditSharp")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RedditSharp")] +[assembly: AssemblyCopyright("Copyright © 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5b1e351d-35b7-443e-9341-52c069a14886")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/RedditSharp/RateLimitException.cs b/RedditSharp/RateLimitException.cs new file mode 100644 index 0000000..ced3c99 --- /dev/null +++ b/RedditSharp/RateLimitException.cs @@ -0,0 +1,15 @@ +using System; + +namespace RedditSharp +{ + public class RateLimitException : Exception + { + public TimeSpan TimeToReset { get; set; } + + public RateLimitException(TimeSpan timeToReset) + { + TimeToReset = timeToReset; + } + } +} + diff --git a/RedditSharp/Reddit.cs b/RedditSharp/Reddit.cs new file mode 100644 index 0000000..c913670 --- /dev/null +++ b/RedditSharp/Reddit.cs @@ -0,0 +1,371 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Linq; +using System.Net; +using System.Security.Authentication; +using RedditSharp.Things; +using System.Threading.Tasks; + +namespace RedditSharp +{ + /// + /// Class to communicate with Reddit.com + /// + public class Reddit + { + #region Constant Urls + + private const string SslLoginUrl = "https://ssl.reddit.com/api/login"; + private const string LoginUrl = "/api/login/username"; + private const string UserInfoUrl = "/user/{0}/about.json"; + private const string MeUrl = "/api/me.json"; + private const string OAuthMeUrl = "/api/v1/me.json"; + private const string SubredditAboutUrl = "/r/{0}/about.json"; + private const string ComposeMessageUrl = "/api/compose"; + private const string RegisterAccountUrl = "/api/register"; + private const string GetThingUrl = "/api/info.json?id={0}"; + private const string GetCommentUrl = "/r/{0}/comments/{1}/foo/{2}.json"; + private const string GetPostUrl = "{0}.json"; + private const string DomainUrl = "www.reddit.com"; + private const string OAuthDomainUrl = "oauth.reddit.com"; + private const string SearchUrl = "/search.json?q={0}&restrict_sr=off&sort={1}&t={2}"; + private const string UrlSearchPattern = "url:'{0}'"; + + #endregion + + #region Static Variables + + static Reddit() + { + WebAgent.UserAgent = ""; + WebAgent.RateLimit = WebAgent.RateLimitMode.Pace; + WebAgent.Protocol = "http"; + WebAgent.RootDomain = "www.reddit.com"; + } + + #endregion + + internal readonly IWebAgent _webAgent; + + /// + /// Captcha solver instance to use when solving captchas. + /// + public ICaptchaSolver CaptchaSolver; + + /// + /// The authenticated user for this instance. + /// + public AuthenticatedUser User { get; set; } + + /// + /// Sets the Rate Limiting Mode of the underlying WebAgent + /// + public WebAgent.RateLimitMode RateLimit + { + get { return WebAgent.RateLimit; } + set { WebAgent.RateLimit = value; } + } + + internal JsonSerializerSettings JsonSerializerSettings { get; set; } + + /// + /// Gets the FrontPage using the current Reddit instance. + /// + public Subreddit FrontPage + { + get { return Subreddit.GetFrontPage(this); } + } + + /// + /// Gets /r/All using the current Reddit instance. + /// + public Subreddit RSlashAll + { + get { return Subreddit.GetRSlashAll(this); } + } + + public Reddit() + { + JsonSerializerSettings = new JsonSerializerSettings + { + CheckAdditionalContent = false, + DefaultValueHandling = DefaultValueHandling.Ignore + }; + _webAgent = new WebAgent(); + CaptchaSolver = new ConsoleCaptchaSolver(); + } + + public Reddit(WebAgent.RateLimitMode limitMode) : this() + { + WebAgent.UserAgent = ""; + WebAgent.RateLimit = limitMode; + WebAgent.RootDomain = "www.reddit.com"; + } + + public Reddit(string username, string password, bool useSsl = true) : this() + { + LogIn(username, password, useSsl); + } + + public Reddit(string accessToken) : this() + { + WebAgent.Protocol = "https"; + WebAgent.RootDomain = OAuthDomainUrl; + _webAgent.AccessToken = accessToken; + InitOrUpdateUser(); + } + + /// + /// Logs in the current Reddit instance. + /// + /// The username of the user to log on to. + /// The password of the user to log on to. + /// Whether to use SSL or not. (default: true) + /// + public AuthenticatedUser LogIn(string username, string password, bool useSsl = true) + { + if (Type.GetType("Mono.Runtime") != null) + ServicePointManager.ServerCertificateValidationCallback = (s, c, ch, ssl) => true; + _webAgent.Cookies = new CookieContainer(); + HttpWebRequest request; + if (useSsl) + request = _webAgent.CreatePost(SslLoginUrl); + else + request = _webAgent.CreatePost(LoginUrl); + var stream = request.GetRequestStream(); + if (useSsl) + { + _webAgent.WritePostBody(stream, new + { + user = username, + passwd = password, + api_type = "json" + }); + } + else + { + _webAgent.WritePostBody(stream, new + { + user = username, + passwd = password, + api_type = "json", + op = "login" + }); + } + stream.Close(); + var response = (HttpWebResponse)request.GetResponse(); + var result = _webAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(result)["json"]; + if (json["errors"].Count() != 0) + throw new AuthenticationException("Incorrect login."); + + InitOrUpdateUser(); + + return User; + } + + public RedditUser GetUser(string name) + { + var request = _webAgent.CreateGet(string.Format(UserInfoUrl, name)); + var response = request.GetResponse(); + var result = _webAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(result); + return new RedditUser().Init(this, json, _webAgent); + } + + /// + /// Initializes the User property if it's null, + /// otherwise replaces the existing user object + /// with a new one fetched from reddit servers. + /// + public void InitOrUpdateUser() + { + var request = _webAgent.CreateGet(string.IsNullOrEmpty(_webAgent.AccessToken) ? MeUrl : OAuthMeUrl); + var response = (HttpWebResponse)request.GetResponse(); + var result = _webAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(result); + User = new AuthenticatedUser().Init(this, json, _webAgent); + } + + #region Obsolete Getter Methods + + [Obsolete("Use User property instead")] + public AuthenticatedUser GetMe() + { + return User; + } + + #endregion Obsolete Getter Methods + + public Subreddit GetSubreddit(string name) + { + if (name.StartsWith("r/")) + name = name.Substring(2); + if (name.StartsWith("/r/")) + name = name.Substring(3); + return GetThing(string.Format(SubredditAboutUrl, name)); + } + + /// + /// Returns the subreddit. + /// + /// The name of the subreddit + /// The Subreddit by given name + public async Task GetSubredditAsync(string name) + { + if (name.StartsWith("r/")) + name = name.Substring(2); + if (name.StartsWith("/r/")) + name = name.Substring(3); + return await GetThingAsync(string.Format(SubredditAboutUrl, name)); + } + + public Domain GetDomain(string domain) + { + if (!domain.StartsWith("http://") && !domain.StartsWith("https://")) + domain = "http://" + domain; + var uri = new Uri(domain); + return new Domain(this, uri, _webAgent); + } + + public JToken GetToken(Uri uri) + { + var url = uri.AbsoluteUri; + + if (url.EndsWith("/")) + url = url.Remove(url.Length - 1); + + var request = _webAgent.CreateGet(string.Format(GetPostUrl, url)); + var response = request.GetResponse(); + var data = _webAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + + return json[0]["data"]["children"].First; + } + + public Post GetPost(Uri uri) + { + return new Post().Init(this, GetToken(uri), _webAgent); + } + + public void ComposePrivateMessage(string subject, string body, string to, string captchaId = "", string captchaAnswer = "") + { + if (User == null) + throw new Exception("User can not be null."); + var request = _webAgent.CreatePost(ComposeMessageUrl); + _webAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + subject, + text = body, + to, + uh = User.Modhash, + iden = captchaId, + captcha = captchaAnswer + }); + var response = request.GetResponse(); + var result = _webAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(result); + + ICaptchaSolver solver = CaptchaSolver; // Prevent race condition + + if (json["json"]["errors"].Any() && json["json"]["errors"][0][0].ToString() == "BAD_CAPTCHA" && solver != null) + { + captchaId = json["json"]["captcha"].ToString(); + CaptchaResponse captchaResponse = solver.HandleCaptcha(new Captcha(captchaId)); + + if (!captchaResponse.Cancel) // Keep trying until we are told to cancel + ComposePrivateMessage(subject, body, to, captchaId, captchaResponse.Answer); + } + } + + /// + /// Registers a new Reddit user + /// + /// The username for the new account. + /// The password for the new account. + /// The optional recovery email for the new account. + /// The newly created user account + public AuthenticatedUser RegisterAccount(string userName, string passwd, string email = "") + { + var request = _webAgent.CreatePost(RegisterAccountUrl); + _webAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + email = email, + passwd = passwd, + passwd2 = passwd, + user = userName + }); + var response = request.GetResponse(); + var result = _webAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(result); + return new AuthenticatedUser().Init(this, json, _webAgent); + // TODO: Error + } + + public Thing GetThingByFullname(string fullname) + { + var request = _webAgent.CreateGet(string.Format(GetThingUrl, fullname)); + var response = request.GetResponse(); + var data = _webAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + return Thing.Parse(this, json["data"]["children"][0], _webAgent); + } + + public Comment GetComment(string subreddit, string name, string linkName) + { + try + { + if (linkName.StartsWith("t3_")) + linkName = linkName.Substring(3); + if (name.StartsWith("t1_")) + name = name.Substring(3); + var request = _webAgent.CreateGet(string.Format(GetCommentUrl, subreddit, linkName, name)); + var response = request.GetResponse(); + var data = _webAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + return Thing.Parse(this, json[1]["data"]["children"][0], _webAgent) as Comment; + } + catch (WebException) + { + return null; + } + } + + public Listing SearchByUrl(string url) where T : Thing + { + var urlSearchQuery = string.Format(UrlSearchPattern, url); + return Search(urlSearchQuery); + } + + public Listing Search(string query) where T : Thing + { + return new Listing(this, string.Format(SearchUrl, query, "relevance", "all"), _webAgent); + } + + #region Helpers + + protected async internal Task GetThingAsync(string url) where T : Thing + { + var request = _webAgent.CreateGet(url); + var response = request.GetResponse(); + var data = _webAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + var ret = await Thing.ParseAsync(this, json, _webAgent); + return (T)ret; + } + + protected internal T GetThing(string url) where T : Thing + { + var request = _webAgent.CreateGet(url); + var response = request.GetResponse(); + var data = _webAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + return (T)Thing.Parse(this, json, _webAgent); + } + + #endregion + } +} diff --git a/RedditSharp/RedditAPINameAttribute.cs b/RedditSharp/RedditAPINameAttribute.cs new file mode 100644 index 0000000..e57999a --- /dev/null +++ b/RedditSharp/RedditAPINameAttribute.cs @@ -0,0 +1,20 @@ +using System; + +namespace RedditSharp +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + internal class RedditAPINameAttribute : Attribute + { + internal string Name { get; private set; } + + internal RedditAPINameAttribute(string name) + { + Name = name; + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/RedditSharp/RedditException.cs b/RedditSharp/RedditException.cs new file mode 100644 index 0000000..b34e71f --- /dev/null +++ b/RedditSharp/RedditException.cs @@ -0,0 +1,59 @@ +using System; +using System.Runtime.Serialization; + +namespace RedditSharp +{ + /// + /// Represents an error that occurred during accessing or manipulating data on Reddit. + /// + [Serializable] + public class RedditException : Exception + { + /// + /// Initializes a new instance of the RedditException class. + /// + public RedditException() + { + + } + + /// + /// Initializes a new instance of the RedditException class with a specified error message. + /// + /// The message that describes the error. + public RedditException(string message) + : base(message) + { + + } + + /// + /// Initializes a new instance of the RedditException class with a specified error message and + /// a referenced inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception, or a null + /// reference (Nothing in Visual Basic) if no inner exception is specified. + public RedditException(string message, Exception inner) + : base(message, inner) + { + + } + + /// + /// Initializes a new instance of the RedditException class with serialized data. + /// + /// The System.Runtime.Serialization.SerializationInfo that holds the + /// serialized object data about the exception being thrown. + /// The System.Runtime.Serialization.StreamingContext that contains + /// contextual information about the source or destination. + /// The info parameter is null. + /// The class name + /// is null or System.Exception.HResult is zero (0). + protected RedditException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + + } + } +} diff --git a/RedditSharp/RedditSharp.csproj b/RedditSharp/RedditSharp.csproj new file mode 100644 index 0000000..2cf001a --- /dev/null +++ b/RedditSharp/RedditSharp.csproj @@ -0,0 +1,108 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {A368CB75-75F0-4489-904D-B5CEBB0FE624} + Library + Properties + RedditSharp + RedditSharp + 512 + v4.5 + + + + True + full + False + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + True + bin\Release\ + TRACE + prompt + 4 + false + + + + + ..\HtmlAgilityPack.dll + + + ..\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RedditSharp/SpamFilterSettings.cs b/RedditSharp/SpamFilterSettings.cs new file mode 100644 index 0000000..5e5b02a --- /dev/null +++ b/RedditSharp/SpamFilterSettings.cs @@ -0,0 +1,16 @@ +namespace RedditSharp +{ + public class SpamFilterSettings + { + public SpamFilterStrength LinkPostStrength { get; set; } + public SpamFilterStrength SelfPostStrength { get; set; } + public SpamFilterStrength CommentStrength { get; set; } + + public SpamFilterSettings() + { + LinkPostStrength = SpamFilterStrength.High; + SelfPostStrength = SpamFilterStrength.High; + CommentStrength = SpamFilterStrength.High; + } + } +} diff --git a/RedditSharp/SubmitData.cs b/RedditSharp/SubmitData.cs new file mode 100644 index 0000000..ce907b1 --- /dev/null +++ b/RedditSharp/SubmitData.cs @@ -0,0 +1,34 @@ +namespace RedditSharp +{ + internal abstract class SubmitData + { + [RedditAPIName("api_type")] + internal string APIType { get; set; } + + [RedditAPIName("kind")] + internal string Kind { get; set; } + + [RedditAPIName("sr")] + internal string Subreddit { get; set; } + + [RedditAPIName("uh")] + internal string UserHash { get; set; } + + [RedditAPIName("title")] + internal string Title { get; set; } + + [RedditAPIName("iden")] + internal string Iden { get; set; } + + [RedditAPIName("captcha")] + internal string Captcha { get; set; } + + [RedditAPIName("resubmit")] + internal bool Resubmit { get; set; } + + protected SubmitData() + { + APIType = "json"; + } + } +} diff --git a/RedditSharp/SubredditImage.cs b/RedditSharp/SubredditImage.cs new file mode 100644 index 0000000..cd4c2fc --- /dev/null +++ b/RedditSharp/SubredditImage.cs @@ -0,0 +1,54 @@ +using System; +namespace RedditSharp +{ + public class SubredditImage + { + private const string DeleteImageUrl = "/api/delete_sr_img"; + + private Reddit Reddit { get; set; } + private IWebAgent WebAgent { get; set; } + + public SubredditImage(Reddit reddit, SubredditStyle subredditStyle, + string cssLink, string name, IWebAgent webAgent) + { + Reddit = reddit; + WebAgent = webAgent; + SubredditStyle = subredditStyle; + Name = name; + CssLink = cssLink; + } + + public SubredditImage(Reddit reddit, SubredditStyle subreddit, + string cssLink, string name, string url, IWebAgent webAgent) + : this(reddit, subreddit, cssLink, name, webAgent) + { + Url = new Uri(url); + // Handle legacy image urls + // http://thumbs.reddit.com/FULLNAME_NUMBER.png + int discarded; + if (int.TryParse(url, out discarded)) + Url = new Uri(string.Format("http://thumbs.reddit.com/{0}_{1}.png", subreddit.Subreddit.FullName, url), UriKind.Absolute); + } + + public string CssLink { get; set; } + public string Name { get; set; } + public Uri Url { get; set; } + public SubredditStyle SubredditStyle { get; set; } + + public void Delete() + { + var request = WebAgent.CreatePost(DeleteImageUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + img_name = Name, + uh = Reddit.User.Modhash, + r = SubredditStyle.Subreddit.Name + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + SubredditStyle.Images.Remove(this); + } + } +} diff --git a/RedditSharp/SubredditSettings.cs b/RedditSharp/SubredditSettings.cs new file mode 100644 index 0000000..65dc529 --- /dev/null +++ b/RedditSharp/SubredditSettings.cs @@ -0,0 +1,264 @@ +using System.Web; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RedditSharp.Things; + +namespace RedditSharp +{ + public class SubredditSettings + { + private const string SiteAdminUrl = "/api/site_admin"; + private const string DeleteHeaderImageUrl = "/api/delete_sr_header"; + + private Reddit Reddit { get; set; } + private IWebAgent WebAgent { get; set; } + + [JsonIgnore] + public Subreddit Subreddit { get; set; } + + public SubredditSettings(Reddit reddit, Subreddit subreddit, IWebAgent webAgent) + { + Subreddit = subreddit; + Reddit = reddit; + WebAgent = webAgent; + // Default settings, for use when reduced information is given + AllowAsDefault = true; + Domain = null; + Sidebar = string.Empty; + Language = "en"; + Title = Subreddit.DisplayName; + WikiEditKarma = 100; + WikiEditAge = 10; + UseDomainCss = false; + UseDomainSidebar = false; + HeaderHoverText = string.Empty; + NSFW = false; + PublicDescription = string.Empty; + WikiEditMode = WikiEditMode.None; + SubredditType = SubredditType.Public; + ShowThumbnails = true; + ContentOptions = ContentOptions.All; + SpamFilter = new SpamFilterSettings(); + } + + public SubredditSettings(Subreddit subreddit, Reddit reddit, JObject json, IWebAgent webAgent) : this(reddit, subreddit, webAgent) + { + var data = json["data"]; + AllowAsDefault = data["default_set"].ValueOrDefault(); + Domain = data["domain"].ValueOrDefault(); + Sidebar = HttpUtility.HtmlDecode(data["description"].ValueOrDefault() ?? string.Empty); + Language = data["language"].ValueOrDefault(); + Title = data["title"].ValueOrDefault(); + WikiEditKarma = data["wiki_edit_karma"].ValueOrDefault(); + UseDomainCss = data["domain_css"].ValueOrDefault(); + UseDomainSidebar = data["domain_sidebar"].ValueOrDefault(); + HeaderHoverText = data["header_hover_text"].ValueOrDefault(); + NSFW = data["over_18"].ValueOrDefault(); + PublicDescription = HttpUtility.HtmlDecode(data["public_description"].ValueOrDefault() ?? string.Empty); + SpamFilter = new SpamFilterSettings + { + LinkPostStrength = GetSpamFilterStrength(data["spam_links"].ValueOrDefault()), + SelfPostStrength = GetSpamFilterStrength(data["spam_selfposts"].ValueOrDefault()), + CommentStrength = GetSpamFilterStrength(data["spam_comments"].ValueOrDefault()) + }; + if (data["wikimode"] != null) + { + var wikiMode = data["wikimode"].ValueOrDefault(); + switch (wikiMode) + { + case "disabled": + WikiEditMode = WikiEditMode.None; + break; + case "modonly": + WikiEditMode = WikiEditMode.Moderators; + break; + case "anyone": + WikiEditMode = WikiEditMode.All; + break; + } + } + if (data["subreddit_type"] != null) + { + var type = data["subreddit_type"].ValueOrDefault(); + switch (type) + { + case "public": + SubredditType = SubredditType.Public; + break; + case "private": + SubredditType = SubredditType.Private; + break; + case "restricted": + SubredditType = SubredditType.Restricted; + break; + } + } + ShowThumbnails = data["show_media"].ValueOrDefault(); + WikiEditAge = data["wiki_edit_age"].ValueOrDefault(); + if (data["content_options"] != null) + { + var contentOptions = data["content_options"].ValueOrDefault(); + switch (contentOptions) + { + case "any": + ContentOptions = ContentOptions.All; + break; + case "link": + ContentOptions = ContentOptions.LinkOnly; + break; + case "self": + ContentOptions = ContentOptions.SelfOnly; + break; + } + } + } + + public bool AllowAsDefault { get; set; } + public string Domain { get; set; } + public string Sidebar { get; set; } + public string Language { get; set; } + public string Title { get; set; } + public int WikiEditKarma { get; set; } + public bool UseDomainCss { get; set; } + public bool UseDomainSidebar { get; set; } + public string HeaderHoverText { get; set; } + public bool NSFW { get; set; } + public string PublicDescription { get; set; } + public WikiEditMode WikiEditMode { get; set; } + public SubredditType SubredditType { get; set; } + public bool ShowThumbnails { get; set; } + public int WikiEditAge { get; set; } + public ContentOptions ContentOptions { get; set; } + public SpamFilterSettings SpamFilter { get; set; } + + public void UpdateSettings() + { + var request = WebAgent.CreatePost(SiteAdminUrl); + var stream = request.GetRequestStream(); + string link_type; + string type; + string wikimode; + switch (ContentOptions) + { + case ContentOptions.All: + link_type = "any"; + break; + case ContentOptions.LinkOnly: + link_type = "link"; + break; + default: + link_type = "self"; + break; + } + switch (SubredditType) + { + case SubredditType.Public: + type = "public"; + break; + case SubredditType.Private: + type = "private"; + break; + default: + type = "restricted"; + break; + } + switch (WikiEditMode) + { + case WikiEditMode.All: + wikimode = "anyone"; + break; + case WikiEditMode.Moderators: + wikimode = "modonly"; + break; + default: + wikimode = "disabled"; + break; + } + WebAgent.WritePostBody(stream, new + { + allow_top = AllowAsDefault, + description = Sidebar, + domain = Domain, + lang = Language, + link_type, + over_18 = NSFW, + public_description = PublicDescription, + show_media = ShowThumbnails, + sr = Subreddit.FullName, + title = Title, + type, + uh = Reddit.User.Modhash, + wiki_edit_age = WikiEditAge, + wiki_edit_karma = WikiEditKarma, + wikimode, + spam_links = SpamFilter == null ? null : SpamFilter.LinkPostStrength.ToString().ToLowerInvariant(), + spam_selfposts = SpamFilter == null ? null : SpamFilter.SelfPostStrength.ToString().ToLowerInvariant(), + spam_comments = SpamFilter == null ? null : SpamFilter.CommentStrength.ToString().ToLowerInvariant(), + api_type = "json" + }, "header-title", HeaderHoverText); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + } + + /// + /// Resets the subreddit's header image to the Reddit logo + /// + public void ResetHeaderImage() + { + var request = WebAgent.CreatePost(DeleteHeaderImageUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + uh = Reddit.User.Modhash, + r = Subreddit.Name + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + } + + private SpamFilterStrength GetSpamFilterStrength(string rawValue) + { + switch(rawValue) + { + case "low": + return SpamFilterStrength.Low; + case "high": + return SpamFilterStrength.High; + case "all": + return SpamFilterStrength.All; + default: + return SpamFilterStrength.High; + } + } + } + + public enum WikiEditMode + { + None, + Moderators, + All + } + + public enum SubredditType + { + Public, + Restricted, + Private + } + + public enum ContentOptions + { + All, + LinkOnly, + SelfOnly + } + + public enum SpamFilterStrength + { + Low, + High, + All + } +} diff --git a/RedditSharp/SubredditStyle.cs b/RedditSharp/SubredditStyle.cs new file mode 100644 index 0000000..254bcc9 --- /dev/null +++ b/RedditSharp/SubredditStyle.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using System.Web; +using RedditSharp.Things; + +namespace RedditSharp +{ + public class SubredditStyle + { + private const string UploadImageUrl = "/api/upload_sr_img"; + private const string UpdateCssUrl = "/api/subreddit_stylesheet"; + + private Reddit Reddit { get; set; } + private IWebAgent WebAgent { get; set; } + + public SubredditStyle(Reddit reddit, Subreddit subreddit, IWebAgent webAgent) + { + Reddit = reddit; + Subreddit = subreddit; + WebAgent = webAgent; + } + + public SubredditStyle(Reddit reddit, Subreddit subreddit, JToken json, IWebAgent webAgent) : this(reddit, subreddit, webAgent) + { + Images = new List(); + var data = json["data"]; + CSS = HttpUtility.HtmlDecode(data["stylesheet"].Value()); + foreach (var image in data["images"]) + { + Images.Add(new SubredditImage( + Reddit, this, image["link"].Value(), + image["name"].Value(), image["url"].Value(), WebAgent)); + } + } + + public string CSS { get; set; } + public List Images { get; set; } + public Subreddit Subreddit { get; set; } + + public void UpdateCss() + { + var request = WebAgent.CreatePost(UpdateCssUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + op = "save", + stylesheet_contents = CSS, + uh = Reddit.User.Modhash, + api_type = "json", + r = Subreddit.Name + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + } + + public void UploadImage(string name, ImageType imageType, byte[] file) + { + var request = WebAgent.CreatePost(UploadImageUrl); + var formData = new MultipartFormBuilder(request); + formData.AddDynamic(new + { + name, + uh = Reddit.User.Modhash, + r = Subreddit.Name, + formid = "image-upload", + img_type = imageType == ImageType.PNG ? "png" : "jpg", + upload = "" + }); + formData.AddFile("file", "foo.png", file, imageType == ImageType.PNG ? "image/png" : "image/jpeg"); + formData.Finish(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + // TODO: Detect errors + } + } + + public enum ImageType + { + PNG, + JPEG + } +} diff --git a/RedditSharp/TextData.cs b/RedditSharp/TextData.cs new file mode 100644 index 0000000..c401a6d --- /dev/null +++ b/RedditSharp/TextData.cs @@ -0,0 +1,13 @@ +namespace RedditSharp +{ + internal class TextData : SubmitData + { + [RedditAPIName("text")] + internal string Text { get; set; } + + internal TextData() + { + Kind = "self"; + } + } +} diff --git a/RedditSharp/Things/AuthenticatedUser.cs b/RedditSharp/Things/AuthenticatedUser.cs new file mode 100644 index 0000000..953dc97 --- /dev/null +++ b/RedditSharp/Things/AuthenticatedUser.cs @@ -0,0 +1,157 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp.Things +{ + public class AuthenticatedUser : RedditUser + { + private const string ModeratorUrl = "/reddits/mine/moderator.json"; + private const string UnreadMessagesUrl = "/message/unread.json?mark=true&limit=25"; + private const string ModQueueUrl = "/r/mod/about/modqueue.json"; + private const string UnmoderatedUrl = "/r/mod/about/unmoderated.json"; + private const string ModMailUrl = "/message/moderator.json"; + private const string MessagesUrl = "/message/messages.json"; + private const string InboxUrl = "/message/inbox.json"; + private const string SentUrl = "/message/sent.json"; + + public new AuthenticatedUser Init(Reddit reddit, JToken json, IWebAgent webAgent) + { + CommonInit(reddit, json, webAgent); + JsonConvert.PopulateObject(json["name"] == null ? json["data"].ToString() : json.ToString(), this, + reddit.JsonSerializerSettings); + return this; + } + public async new Task InitAsync(Reddit reddit, JToken json, IWebAgent webAgent) + { + CommonInit(reddit, json, webAgent); + await Task.Factory.StartNew(() => JsonConvert.PopulateObject(json["name"] == null ? json["data"].ToString() : json.ToString(), this, + reddit.JsonSerializerSettings)); + return this; + } + + private void CommonInit(Reddit reddit, JToken json, IWebAgent webAgent) + { + base.Init(reddit, json, webAgent); + } + + public Listing ModeratorSubreddits + { + get + { + return new Listing(Reddit, ModeratorUrl, WebAgent); + } + } + + public Listing UnreadMessages + { + get + { + return new Listing(Reddit, UnreadMessagesUrl, WebAgent); + } + } + + public Listing ModerationQueue + { + get + { + return new Listing(Reddit, ModQueueUrl, WebAgent); + } + } + + public Listing UnmoderatedLinks + { + get + { + return new Listing(Reddit, UnmoderatedUrl, WebAgent); + } + } + + public Listing ModMail + { + get + { + return new Listing(Reddit, ModMailUrl, WebAgent); + } + } + + public Listing PrivateMessages + { + get + { + return new Listing(Reddit, MessagesUrl, WebAgent); + } + } + + public Listing Inbox + { + get + { + return new Listing(Reddit, InboxUrl, WebAgent); + } + } + + public Listing Sent + { + get + { + return new Listing(Reddit, SentUrl, WebAgent); + } + } + + #region Obsolete Getter Methods + + [Obsolete("Use ModeratorSubreddits property instead")] + public Listing GetModeratorReddits() + { + return ModeratorSubreddits; + } + + [Obsolete("Use UnreadMessages property instead")] + public Listing GetUnreadMessages() + { + return UnreadMessages; + } + + [Obsolete("Use ModerationQueue property instead")] + public Listing GetModerationQueue() + { + return new Listing(Reddit, ModQueueUrl, WebAgent); + } + + public Listing GetUnmoderatedLinks() + { + return new Listing(Reddit, UnmoderatedUrl, WebAgent); + } + + [Obsolete("Use ModMail property instead")] + public Listing GetModMail() + { + return new Listing(Reddit, ModMailUrl, WebAgent); + } + + [Obsolete("Use PrivateMessages property instead")] + public Listing GetPrivateMessages() + { + return new Listing(Reddit, MessagesUrl, WebAgent); + } + + [Obsolete("Use Inbox property instead")] + public Listing GetInbox() + { + return new Listing(Reddit, InboxUrl, WebAgent); + } + + #endregion Obsolete Getter Methods + + [JsonProperty("modhash")] + public string Modhash { get; set; } + + [JsonProperty("has_mail")] + public bool HasMail { get; set; } + + [JsonProperty("has_mod_mail")] + public bool HasModMail { get; set; } + } +} diff --git a/RedditSharp/Things/Comment.cs b/RedditSharp/Things/Comment.cs new file mode 100644 index 0000000..78e6965 --- /dev/null +++ b/RedditSharp/Things/Comment.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Authentication; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp.Things +{ + public class Comment : VotableThing + { + private const string CommentUrl = "/api/comment"; + private const string DistinguishUrl = "/api/distinguish"; + private const string EditUserTextUrl = "/api/editusertext"; + private const string RemoveUrl = "/api/remove"; + private const string SetAsReadUrl = "/api/read_message"; + + [JsonIgnore] + private Reddit Reddit { get; set; } + [JsonIgnore] + private IWebAgent WebAgent { get; set; } + + public Comment Init(Reddit reddit, JToken json, IWebAgent webAgent, Thing sender) + { + var data = CommonInit(reddit, json, webAgent, sender); + ParseComments(reddit, json, webAgent, sender); + JsonConvert.PopulateObject(data.ToString(), this, reddit.JsonSerializerSettings); + return this; + } + public async Task InitAsync(Reddit reddit, JToken json, IWebAgent webAgent, Thing sender) + { + var data = CommonInit(reddit, json, webAgent, sender); + await ParseCommentsAsync(reddit, json, webAgent, sender); + await Task.Factory.StartNew(() => JsonConvert.PopulateObject(data.ToString(), this, reddit.JsonSerializerSettings)); + return this; + } + + private JToken CommonInit(Reddit reddit, JToken json, IWebAgent webAgent, Thing sender) + { + base.Init(reddit, webAgent, json); + var data = json["data"]; + Reddit = reddit; + WebAgent = webAgent; + this.Parent = sender; + + // Handle Reddit's API being horrible + if (data["context"] != null) + { + var context = data["context"].Value(); + LinkId = context.Split('/')[4]; + } + + return data; + } + + private void ParseComments(Reddit reddit, JToken data, IWebAgent webAgent, Thing sender) + { + // Parse sub comments + var replies = data["data"]["replies"]; + var subComments = new List(); + if (replies != null && replies.Count() > 0) + { + foreach (var comment in replies["data"]["children"]) + subComments.Add(new Comment().Init(reddit, comment, webAgent, sender)); + } + Comments = subComments.ToArray(); + } + + private async Task ParseCommentsAsync(Reddit reddit, JToken data, IWebAgent webAgent, Thing sender) + { + // Parse sub comments + var replies = data["data"]["replies"]; + var subComments = new List(); + if (replies != null && replies.Count() > 0) + { + foreach (var comment in replies["data"]["children"]) + subComments.Add(await new Comment().InitAsync(reddit, comment, webAgent, sender)); + } + Comments = subComments.ToArray(); + } + + [JsonProperty("author")] + public string Author { get; set; } + [JsonProperty("banned_by")] + public string BannedBy { get; set; } + [JsonProperty("body")] + public string Body { get; set; } + [JsonProperty("body_html")] + public string BodyHtml { get; set; } + [JsonProperty("parent_id")] + public string ParentId { get; set; } + [JsonProperty("subreddit")] + public string Subreddit { get; set; } + [JsonProperty("approved_by")] + public string ApprovedBy { get; set; } + [JsonProperty("author_flair_css_class")] + public string AuthorFlairCssClass { get; set; } + [JsonProperty("author_flair_text")] + public string AuthorFlairText { get; set; } + [JsonProperty("gilded")] + public int Gilded { get; set; } + [JsonProperty("link_id")] + public string LinkId { get; set; } + [JsonProperty("link_title")] + public string LinkTitle { get; set; } + [JsonProperty("num_reports")] + public int? NumReports { get; set; } + [JsonProperty("distinguished")] + [JsonConverter(typeof(DistinguishConverter))] + public DistinguishType Distinguished { get; set; } + + [JsonIgnore] + public IList Comments { get; private set; } + + [JsonIgnore] + public Thing Parent { get; internal set; } + + public override string Shortlink + { + get + { + // Not really a "short" link, but you can't actually use short links for comments + string linkId = ""; + int index = this.LinkId.IndexOf('_'); + if (index > -1) + { + linkId = this.LinkId.Substring(index + 1); + } + + return String.Format("{0}://{1}/r/{2}/comments/{3}/_/{4}", + RedditSharp.WebAgent.Protocol, RedditSharp.WebAgent.RootDomain, + this.Subreddit, this.Parent != null ? this.Parent.Id : linkId, this.Id); + } + } + + public Comment Reply(string message) + { + if (Reddit.User == null) + throw new AuthenticationException("No user logged in."); + var request = WebAgent.CreatePost(CommentUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + text = message, + thing_id = FullName, + uh = Reddit.User.Modhash, + api_type = "json" + //r = Subreddit + }); + stream.Close(); + try + { + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(data); + if (json["json"]["ratelimit"] != null) + throw new RateLimitException(TimeSpan.FromSeconds(json["json"]["ratelimit"].ValueOrDefault())); + return new Comment().Init(Reddit, json["json"]["data"]["things"][0], WebAgent, this); + } + catch (WebException ex) + { + var error = new StreamReader(ex.Response.GetResponseStream()).ReadToEnd(); + return null; + } + } + + public void Distinguish(DistinguishType distinguishType) + { + if (Reddit.User == null) + throw new AuthenticationException("No user logged in."); + var request = WebAgent.CreatePost(DistinguishUrl); + var stream = request.GetRequestStream(); + string how; + switch (distinguishType) + { + case DistinguishType.Admin: + how = "admin"; + break; + case DistinguishType.Moderator: + how = "yes"; + break; + case DistinguishType.None: + how = "no"; + break; + default: + how = "special"; + break; + } + WebAgent.WritePostBody(stream, new + { + how, + id = Id, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(data); + if (json["jquery"].Count(i => i[0].Value() == 11 && i[1].Value() == 12) == 0) + throw new AuthenticationException("You are not permitted to distinguish this comment."); + } + + /// + /// Replaces the text in this comment with the input text. + /// + /// The text to replace the comment's contents + public void EditText(string newText) + { + if (Reddit.User == null) + throw new Exception("No user logged in."); + + var request = WebAgent.CreatePost(EditUserTextUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + text = newText, + thing_id = FullName, + uh = Reddit.User.Modhash + }); + var response = request.GetResponse(); + var result = WebAgent.GetResponseString(response.GetResponseStream()); + JToken json = JToken.Parse(result); + if (json["json"].ToString().Contains("\"errors\": []")) + Body = newText; + else + throw new Exception("Error editing text."); + } + + public void Remove() + { + RemoveImpl(false); + } + + public void RemoveSpam() + { + RemoveImpl(true); + } + + private void RemoveImpl(bool spam) + { + var request = WebAgent.CreatePost(RemoveUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + id = FullName, + spam = spam, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + } + + public void SetAsRead() + { + var request = WebAgent.CreatePost(SetAsReadUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + id = FullName, + uh = Reddit.User.Modhash, + api_type = "json" + }); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + } + } + + public enum DistinguishType + { + Moderator, + Admin, + Special, + None + } + + internal class DistinguishConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(DistinguishType) || objectType == typeof(string); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + var value = token.Value(); + if (value == null) + return DistinguishType.None; + switch (value) + { + case "moderator": return DistinguishType.Moderator; + case "admin": return DistinguishType.Admin; + case "special": return DistinguishType.Special; + default: return DistinguishType.None; + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var d = (DistinguishType)value; + if (d == DistinguishType.None) + { + writer.WriteNull(); + return; + } + writer.WriteValue(d.ToString().ToLower()); + } + } + +} diff --git a/RedditSharp/Things/CreatedThing.cs b/RedditSharp/Things/CreatedThing.cs new file mode 100644 index 0000000..894f371 --- /dev/null +++ b/RedditSharp/Things/CreatedThing.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp.Things +{ + public class CreatedThing : Thing + { + private Reddit Reddit { get; set; } + + protected CreatedThing Init(Reddit reddit, JToken json) + { + CommonInit(reddit, json); + JsonConvert.PopulateObject(json["data"].ToString(), this, reddit.JsonSerializerSettings); + return this; + } + protected async Task InitAsync(Reddit reddit, JToken json) + { + CommonInit(reddit, json); + await Task.Factory.StartNew(() => JsonConvert.PopulateObject(json["data"].ToString(), this, reddit.JsonSerializerSettings)); + return this; + } + + private void CommonInit(Reddit reddit, JToken json) + { + base.Init(json); + Reddit = reddit; + } + + + [JsonProperty("created")] + [JsonConverter(typeof(UnixTimestampConverter))] + public DateTime Created { get; set; } + + [JsonProperty("created_utc")] + [JsonConverter(typeof(UnixTimestampConverter))] + public DateTime CreatedUTC { get; set; } + } +} diff --git a/RedditSharp/Things/Post.cs b/RedditSharp/Things/Post.cs new file mode 100644 index 0000000..4d86af6 --- /dev/null +++ b/RedditSharp/Things/Post.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Authentication; +using System.Threading.Tasks; +using System.Web; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp.Things +{ + public class Post : VotableThing + { + private const string CommentUrl = "/api/comment"; + private const string RemoveUrl = "/api/remove"; + private const string DelUrl = "/api/del"; + private const string GetCommentsUrl = "/comments/{0}.json"; + private const string ApproveUrl = "/api/approve"; + private const string EditUserTextUrl = "/api/editusertext"; + private const string HideUrl = "/api/hide"; + private const string UnhideUrl = "/api/unhide"; + private const string SetFlairUrl = "/api/flair"; + private const string MarkNSFWUrl = "/api/marknsfw"; + private const string UnmarkNSFWUrl = "/api/unmarknsfw"; + private const string ContestModeUrl = "/api/set_contest_mode"; + + [JsonIgnore] + private Reddit Reddit { get; set; } + + [JsonIgnore] + private IWebAgent WebAgent { get; set; } + + public Post Init(Reddit reddit, JToken post, IWebAgent webAgent) + { + CommonInit(reddit, post, webAgent); + JsonConvert.PopulateObject(post["data"].ToString(), this, reddit.JsonSerializerSettings); + return this; + } + public async Task InitAsync(Reddit reddit, JToken post, IWebAgent webAgent) + { + CommonInit(reddit, post, webAgent); + await Task.Factory.StartNew(() => JsonConvert.PopulateObject(post["data"].ToString(), this, reddit.JsonSerializerSettings)); + return this; + } + + private void CommonInit(Reddit reddit, JToken post, IWebAgent webAgent) + { + base.Init(reddit, webAgent, post); + Reddit = reddit; + WebAgent = webAgent; + } + + [JsonProperty("author")] + public string AuthorName { get; set; } + + [JsonIgnore] + public RedditUser Author + { + get + { + return Reddit.GetUser(AuthorName); + } + } + + public Comment[] Comments + { + get + { + return ListComments().ToArray(); + } + } + + [JsonProperty("approved_by")] + public string ApprovedBy { get; set; } + + [JsonProperty("author_flair_css_class")] + public string AuthorFlairCssClass { get; set; } + + [JsonProperty("author_flair_text")] + public string AuthorFlairText { get; set; } + + [JsonProperty("banned_by")] + public string BannedBy { get; set; } + + [JsonProperty("domain")] + public string Domain { get; set; } + + [JsonProperty("edited")] + public bool Edited { get; set; } + + [JsonProperty("is_self")] + public bool IsSelfPost { get; set; } + + [JsonProperty("link_flair_css_class")] + public string LinkFlairCssClass { get; set; } + + [JsonProperty("link_flair_text")] + public string LinkFlairText { get; set; } + + [JsonProperty("num_comments")] + public int CommentCount { get; set; } + + [JsonProperty("over_18")] + public bool NSFW { get; set; } + + [JsonProperty("permalink")] + [JsonConverter(typeof(UrlParser))] + public Uri Permalink { get; set; } + + [JsonProperty("score")] + public int Score { get; set; } + + [JsonProperty("selftext")] + public string SelfText { get; set; } + + [JsonProperty("selftext_html")] + public string SelfTextHtml { get; set; } + + [JsonProperty("subreddit")] + public string Subreddit { get; set; } + + [JsonProperty("thumbnail")] + [JsonConverter(typeof(UrlParser))] + public Uri Thumbnail { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("url")] + [JsonConverter(typeof(UrlParser))] + public Uri Url { get; set; } + + [JsonProperty("num_reports")] + public int? Reports { get; set; } + + public Comment Comment(string message) + { + if (Reddit.User == null) + throw new AuthenticationException("No user logged in."); + var request = WebAgent.CreatePost(CommentUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + text = message, + thing_id = FullName, + uh = Reddit.User.Modhash, + api_type = "json" + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(data); + if (json["json"]["ratelimit"] != null) + throw new RateLimitException(TimeSpan.FromSeconds(json["json"]["ratelimit"].ValueOrDefault())); + return new Comment().Init(Reddit, json["json"]["data"]["things"][0], WebAgent, this); + } + + private string SimpleAction(string endpoint) + { + if (Reddit.User == null) + throw new AuthenticationException("No user logged in."); + var request = WebAgent.CreatePost(endpoint); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + id = FullName, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + return data; + } + + private string SimpleActionToggle(string endpoint, bool value) + { + if (Reddit.User == null) + throw new AuthenticationException("No user logged in."); + var request = WebAgent.CreatePost(endpoint); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + id = FullName, + state = value, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + return data; + } + + public void Approve() + { + var data = SimpleAction(ApproveUrl); + } + + public void Remove() + { + RemoveImpl(false); + } + + public void RemoveSpam() + { + RemoveImpl(true); + } + + private void RemoveImpl(bool spam) + { + var request = WebAgent.CreatePost(RemoveUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + id = FullName, + spam = spam, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + } + + public void Del() + { + var data = SimpleAction(ApproveUrl); + } + + public void Hide() + { + var data = SimpleAction(HideUrl); + } + + public void Unhide() + { + var data = SimpleAction(UnhideUrl); + } + + public void MarkNSFW() + { + var data = SimpleAction(MarkNSFWUrl); + } + + public void UnmarkNSFW() + { + var data = SimpleAction(UnmarkNSFWUrl); + } + + public void ContestMode(bool state) + { + var data = SimpleActionToggle(ContestModeUrl, state); + } + + #region Obsolete Getter Methods + + [Obsolete("Use Comments property instead")] + public Comment[] GetComments() + { + return Comments; + } + + #endregion Obsolete Getter Methods + + /// + /// Replaces the text in this post with the input text. + /// + /// The text to replace the post's contents + public void EditText(string newText) + { + if (Reddit.User == null) + throw new Exception("No user logged in."); + if (!IsSelfPost) + throw new Exception("Submission to edit is not a self-post."); + + var request = WebAgent.CreatePost(EditUserTextUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + text = newText, + thing_id = FullName, + uh = Reddit.User.Modhash + }); + var response = request.GetResponse(); + var result = WebAgent.GetResponseString(response.GetResponseStream()); + JToken json = JToken.Parse(result); + if (json["json"].ToString().Contains("\"errors\": []")) + SelfText = newText; + else + throw new Exception("Error editing text."); + } + public void Update() + { + JToken post = Reddit.GetToken(this.Url); + JsonConvert.PopulateObject(post["data"].ToString(), this, Reddit.JsonSerializerSettings); + } + + public void SetFlair(string flairText, string flairClass) + { + if (Reddit.User == null) + throw new Exception("No user logged in."); + + var request = WebAgent.CreatePost(SetFlairUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + r = Subreddit, + css_class = flairClass, + link = FullName, + //name = Name, + text = flairText, + uh = Reddit.User.Modhash + }); + var response = request.GetResponse(); + var result = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(result); + LinkFlairText = flairText; + } + + public List ListComments(int? limit = null) + { + var url = string.Format(GetCommentsUrl, Id); + + if (limit.HasValue) + { + var query = HttpUtility.ParseQueryString(string.Empty); + query.Add("limit", limit.Value.ToString()); + url = string.Format("{0}?{1}", url, query); + } + + var request = WebAgent.CreateGet(url); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JArray.Parse(data); + var postJson = json.Last()["data"]["children"]; + + var comments = new List(); + foreach (var comment in postJson) + { + comments.Add(new Comment().Init(Reddit, comment, WebAgent, this)); + } + + return comments; + } + } +} diff --git a/RedditSharp/Things/PrivateMessage.cs b/RedditSharp/Things/PrivateMessage.cs new file mode 100644 index 0000000..7d0535a --- /dev/null +++ b/RedditSharp/Things/PrivateMessage.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Authentication; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp.Things +{ + public class PrivateMessage : Thing + { + private const string SetAsReadUrl = "/api/read_message"; + private const string CommentUrl = "/api/comment"; + + private Reddit Reddit { get; set; } + private IWebAgent WebAgent { get; set; } + + [JsonProperty("body")] + public string Body { get; set; } + + [JsonProperty("body_html")] + public string BodyHtml { get; set; } + + [JsonProperty("was_comment")] + public bool IsComment { get; set; } + + [JsonProperty("created")] + [JsonConverter(typeof(UnixTimestampConverter))] + public DateTime Sent { get; set; } + + [JsonProperty("created_utc")] + [JsonConverter(typeof(UnixTimestampConverter))] + public DateTime SentUTC { get; set; } + + [JsonProperty("dest")] + public string Destination { get; set; } + + [JsonProperty("author")] + public string Author { get; set; } + + [JsonProperty("subreddit")] + public string Subreddit { get; set; } + + [JsonProperty("new")] + public bool Unread { get; set; } + + [JsonProperty("subject")] + public string Subject { get; set; } + + [JsonProperty("parent_id")] + public string ParentID { get; set; } + + [JsonProperty("first_message_name")] + public string FirstMessageName { get; set; } + + [JsonIgnore] + public PrivateMessage[] Replies { get; set; } + + [JsonIgnore] + public PrivateMessage Parent + { + get + { + if (string.IsNullOrEmpty(ParentID)) + return null; + var id = ParentID.Remove(0, 3); + var listing = new Listing(Reddit, "/message/messages/" + id + ".json", WebAgent); + var firstMessage = listing.First(); + if (firstMessage.FullName == ParentID) + return listing.First(); + else + return firstMessage.Replies.First(x => x.FullName == ParentID); + } + } + + public Listing Thread + { + get + { + if (string.IsNullOrEmpty(ParentID)) + return null; + var id = ParentID.Remove(0, 3); + return new Listing(Reddit, "/message/messages/" + id + ".json", WebAgent); + } + } + + public PrivateMessage Init(Reddit reddit, JToken json, IWebAgent webAgent) + { + CommonInit(reddit, json, webAgent); + JsonConvert.PopulateObject(json["data"].ToString(), this, reddit.JsonSerializerSettings); + return this; + } + public async Task InitAsync(Reddit reddit, JToken json, IWebAgent webAgent) + { + CommonInit(reddit, json, webAgent); + await Task.Factory.StartNew(() => JsonConvert.PopulateObject(json["data"].ToString(), this, reddit.JsonSerializerSettings)); + return this; + } + + private void CommonInit(Reddit reddit, JToken json, IWebAgent webAgent) + { + base.Init(json); + Reddit = reddit; + WebAgent = webAgent; + var data = json["data"]; + if (data["replies"] != null && data["replies"].Any()) + { + if (data["replies"]["data"] != null) + { + if (data["replies"]["data"]["children"] != null) + { + var replies = new List(); + foreach (var reply in data["replies"]["data"]["children"]) + replies.Add(new PrivateMessage().Init(reddit, reply, webAgent)); + Replies = replies.ToArray(); + } + } + } + } + + #region Obsolete Getter Methods + + [Obsolete("Use Thread property instead")] + public Listing GetThread() + { + return Thread; + } + + #endregion Obsolete Gettter Methods + + public void SetAsRead() + { + var request = WebAgent.CreatePost(SetAsReadUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + id = FullName, + uh = Reddit.User.Modhash, + api_type = "json" + }); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + } + + public void Reply(string message) + { + if (Reddit.User == null) + throw new AuthenticationException("No user logged in."); + var request = WebAgent.CreatePost(CommentUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + text = message, + thing_id = FullName, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(data); + } + } +} diff --git a/RedditSharp/Things/RedditUser.cs b/RedditSharp/Things/RedditUser.cs new file mode 100644 index 0000000..9edefd9 --- /dev/null +++ b/RedditSharp/Things/RedditUser.cs @@ -0,0 +1,201 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp.Things +{ + public class RedditUser : Thing + { + private const string OverviewUrl = "/user/{0}.json"; + private const string CommentsUrl = "/user/{0}/comments.json"; + private const string LinksUrl = "/user/{0}/submitted.json"; + private const string SubscribedSubredditsUrl = "/subreddits/mine.json"; + private const string LikedUrl = "/user/{0}/liked.json"; + private const string DislikedUrl = "/user/{0}/disliked.json"; + + private const int MAX_LIMIT = 100; + + public RedditUser Init(Reddit reddit, JToken json, IWebAgent webAgent) + { + CommonInit(reddit, json, webAgent); + JsonConvert.PopulateObject(json["name"] == null ? json["data"].ToString() : json.ToString(), this, + reddit.JsonSerializerSettings); + return this; + } + public async Task InitAsync(Reddit reddit, JToken json, IWebAgent webAgent) + { + CommonInit(reddit, json, webAgent); + await Task.Factory.StartNew(() => JsonConvert.PopulateObject(json["name"] == null ? json["data"].ToString() : json.ToString(), this, + reddit.JsonSerializerSettings)); + return this; + } + + private void CommonInit(Reddit reddit, JToken json, IWebAgent webAgent) + { + base.Init(json); + Reddit = reddit; + WebAgent = webAgent; + } + + [JsonIgnore] + protected Reddit Reddit { get; set; } + + [JsonIgnore] + protected IWebAgent WebAgent { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("is_gold")] + public bool HasGold { get; set; } + + [JsonProperty("is_mod")] + public bool IsModerator { get; set; } + + [JsonProperty("link_karma")] + public int LinkKarma { get; set; } + + [JsonProperty("comment_karma")] + public int CommentKarma { get; set; } + + [JsonProperty("created")] + [JsonConverter(typeof(UnixTimestampConverter))] + public DateTime Created { get; set; } + + public Listing Overview + { + get + { + return new Listing(Reddit, string.Format(OverviewUrl, Name), WebAgent); + } + } + + public Listing LikedPosts + { + get + { + return new Listing(Reddit, string.Format(LikedUrl, Name), WebAgent); + } + } + + public Listing DislikedPosts + { + get + { + return new Listing(Reddit, string.Format(DislikedUrl, Name), WebAgent); + } + } + + public Listing Comments + { + get + { + return new Listing(Reddit, string.Format(CommentsUrl, Name), WebAgent); + } + } + + public Listing Posts + { + get + { + return new Listing(Reddit, string.Format(LinksUrl, Name), WebAgent); + } + } + + public Listing SubscribedSubreddits + { + get + { + return new Listing(Reddit, SubscribedSubredditsUrl, WebAgent); + } + } + + /// + /// Get a listing of comments from the user sorted by , from time + /// and limited to . + /// + /// How to sort the comments (hot, new, top, controversial). + /// How many comments to fetch per request. Max is 100. + /// What time frame of comments to show (hour, day, week, month, year, all). + /// The listing of comments requested. + public Listing GetComments(Sort sorting = Sort.New, int limit = 25, FromTime fromTime = FromTime.All) + { + if ((limit < 1) || (limit > MAX_LIMIT)) + throw new ArgumentOutOfRangeException("limit", "Valid range: [1," + MAX_LIMIT + "]"); + string commentsUrl = string.Format(CommentsUrl, Name); + commentsUrl += string.Format("?sort={0}&limit={1}&t={2}", Enum.GetName(typeof(Sort), sorting), limit, Enum.GetName(typeof(FromTime), fromTime)); + + return new Listing(Reddit, commentsUrl, WebAgent); + } + + /// + /// Get a listing of posts from the user sorted by , from time + /// and limited to . + /// + /// How to sort the posts (hot, new, top, controversial). + /// How many posts to fetch per request. Max is 100. + /// What time frame of posts to show (hour, day, week, month, year, all). + /// The listing of posts requested. + public Listing GetPosts(Sort sorting = Sort.New, int limit = 25, FromTime fromTime = FromTime.All) + { + if ((limit < 1) || (limit > 100)) + throw new ArgumentOutOfRangeException("limit", "Valid range: [1,100]"); + string linksUrl = string.Format(LinksUrl, Name); + linksUrl += string.Format("?sort={0}&limit={1}&t={2}", Enum.GetName(typeof(Sort), sorting), limit, Enum.GetName(typeof(FromTime), fromTime)); + + return new Listing(Reddit, linksUrl, WebAgent); + } + + public override string ToString() + { + return Name; + } + + #region Obsolete Getter Methods + + [Obsolete("Use Overview property instead")] + public Listing GetOverview() + { + return Overview; + } + + [Obsolete("Use Comments property instead")] + public Listing GetComments() + { + return Comments; + } + + [Obsolete("Use Posts property instead")] + public Listing GetPosts() + { + return Posts; + } + + [Obsolete("Use SubscribedSubreddits property instead")] + public Listing GetSubscribedSubreddits() + { + return SubscribedSubreddits; + } + + #endregion Obsolete Getter Methods + } + + public enum Sort + { + New, + Hot, + Top, + Controversial + } + + public enum FromTime + { + All, + Year, + Month, + Week, + Day, + Hour + } +} diff --git a/RedditSharp/Things/Subreddit.cs b/RedditSharp/Things/Subreddit.cs new file mode 100644 index 0000000..e7856f6 --- /dev/null +++ b/RedditSharp/Things/Subreddit.cs @@ -0,0 +1,677 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Authentication; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp.Things +{ + public class Subreddit : Thing + { + private const string SubredditPostUrl = "/r/{0}.json"; + private const string SubredditNewUrl = "/r/{0}/new.json?sort=new"; + private const string SubredditHotUrl = "/r/{0}/hot.json"; + private const string SubredditTopUrl = "/r/{0}/top.json?t={1}"; + private const string SubscribeUrl = "/api/subscribe"; + private const string GetSettingsUrl = "/r/{0}/about/edit.json"; + private const string GetReducedSettingsUrl = "/r/{0}/about.json"; + private const string ModqueueUrl = "/r/{0}/about/modqueue.json"; + private const string UnmoderatedUrl = "/r/{0}/about/unmoderated.json"; + private const string FlairTemplateUrl = "/api/flairtemplate"; + private const string ClearFlairTemplatesUrl = "/api/clearflairtemplates"; + private const string SetUserFlairUrl = "/api/flair"; + private const string StylesheetUrl = "/r/{0}/about/stylesheet.json"; + private const string UploadImageUrl = "/api/upload_sr_img"; + private const string FlairSelectorUrl = "/api/flairselector"; + private const string AcceptModeratorInviteUrl = "/api/accept_moderator_invite"; + private const string LeaveModerationUrl = "/api/unfriend"; + private const string BanUserUrl = "/api/friend"; + private const string AddModeratorUrl = "/api/friend"; + private const string AddContributorUrl = "/api/friend"; + private const string ModeratorsUrl = "/r/{0}/about/moderators.json"; + private const string FrontPageUrl = "/.json"; + private const string SubmitLinkUrl = "/api/submit"; + private const string FlairListUrl = "/r/{0}/api/flairlist.json"; + private const string CommentsUrl = "/r/{0}/comments.json"; + private const string SearchUrl = "/r/{0}/search.json?q={1}&restrict_sr=on&sort={2}&t={3}"; + + [JsonIgnore] + private Reddit Reddit { get; set; } + + [JsonIgnore] + private IWebAgent WebAgent { get; set; } + + [JsonIgnore] + public Wiki Wiki { get; private set; } + + [JsonProperty("created")] + [JsonConverter(typeof(UnixTimestampConverter))] + public DateTime? Created { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("description_html")] + public string DescriptionHTML { get; set; } + + [JsonProperty("display_name")] + public string DisplayName { get; set; } + + [JsonProperty("header_img")] + public string HeaderImage { get; set; } + + [JsonProperty("header_title")] + public string HeaderTitle { get; set; } + + [JsonProperty("over18")] + public bool? NSFW { get; set; } + + [JsonProperty("public_description")] + public string PublicDescription { get; set; } + + [JsonProperty("subscribers")] + public int? Subscribers { get; set; } + + [JsonProperty("accounts_active")] + public int? ActiveUsers { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("url")] + [JsonConverter(typeof(UrlParser))] + public Uri Url { get; set; } + + [JsonIgnore] + public string Name { get; set; } + + public Listing GetTop(FromTime timePeriod) + { + if (Name == "/") + { + return new Listing(Reddit, "/top.json?t=" + Enum.GetName(typeof(FromTime), timePeriod).ToLower(), WebAgent); + } + return new Listing(Reddit, string.Format(SubredditTopUrl, Name, Enum.GetName(typeof(FromTime), timePeriod)).ToLower(), WebAgent); + } + + public Listing Posts + { + get + { + if (Name == "/") + return new Listing(Reddit, "/.json", WebAgent); + return new Listing(Reddit, string.Format(SubredditPostUrl, Name), WebAgent); + } + } + + public Listing Comments + { + get + { + if (Name == "/") + return new Listing(Reddit, "/comments.json", WebAgent); + return new Listing(Reddit, string.Format(CommentsUrl, Name), WebAgent); + } + } + + public Listing New + { + get + { + if (Name == "/") + return new Listing(Reddit, "/new.json", WebAgent); + return new Listing(Reddit, string.Format(SubredditNewUrl, Name), WebAgent); + } + } + + public Listing Hot + { + get + { + if (Name == "/") + return new Listing(Reddit, "/.json", WebAgent); + return new Listing(Reddit, string.Format(SubredditHotUrl, Name), WebAgent); + } + } + + public Listing ModQueue + { + get + { + return new Listing(Reddit, string.Format(ModqueueUrl, Name), WebAgent); + } + } + + public Listing UnmoderatedLinks + { + get + { + return new Listing(Reddit, string.Format(UnmoderatedUrl, Name), WebAgent); + } + } + + public Listing Search(string terms) + { + return new Listing(Reddit, string.Format(SearchUrl, Name, Uri.EscapeUriString(terms), "relevance", "all"), WebAgent); + } + + public SubredditSettings Settings + { + get + { + if (Reddit.User == null) + throw new AuthenticationException("No user logged in."); + try + { + var request = WebAgent.CreateGet(string.Format(GetSettingsUrl, Name)); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(data); + return new SubredditSettings(this, Reddit, json, WebAgent); + } + catch // TODO: More specific catch + { + // Do it unauthed + var request = WebAgent.CreateGet(string.Format(GetReducedSettingsUrl, Name)); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(data); + return new SubredditSettings(this, Reddit, json, WebAgent); + } + } + } + + public UserFlairTemplate[] UserFlairTemplates // Hacky, there isn't a proper endpoint for this + { + get + { + var request = WebAgent.CreatePost(FlairSelectorUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + name = Reddit.User.Name, + r = Name, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var document = new HtmlDocument(); + document.LoadHtml(data); + if (document.DocumentNode.Descendants("div").First().Attributes["error"] != null) + throw new InvalidOperationException("This subreddit does not allow users to select flair."); + var templateNodes = document.DocumentNode.Descendants("li"); + var list = new List(); + foreach (var node in templateNodes) + { + list.Add(new UserFlairTemplate + { + CssClass = node.Descendants("span").First().Attributes["class"].Value.Split(' ')[1], + Text = node.Descendants("span").First().InnerText + }); + } + return list.ToArray(); + } + } + + public SubredditStyle Stylesheet + { + get + { + var request = WebAgent.CreateGet(string.Format(StylesheetUrl, Name)); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + return new SubredditStyle(Reddit, this, json, WebAgent); + } + } + + public IEnumerable Moderators + { + get + { + var request = WebAgent.CreateGet(string.Format(ModeratorsUrl, Name)); + var response = request.GetResponse(); + var responseString = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JObject.Parse(responseString); + var type = json["kind"].ToString(); + if (type != "UserList") + throw new FormatException("Reddit responded with an object that is not a user listing."); + var data = json["data"]; + var mods = data["children"].ToArray(); + var result = new ModeratorUser[mods.Length]; + for (var i = 0; i < mods.Length; i++) + { + var mod = new ModeratorUser(Reddit, mods[i]); + result[i] = mod; + } + return result; + } + } + + public Subreddit Init(Reddit reddit, JToken json, IWebAgent webAgent) + { + CommonInit(reddit, json, webAgent); + JsonConvert.PopulateObject(json["data"].ToString(), this, reddit.JsonSerializerSettings); + SetName(); + + return this; + } + + public async Task InitAsync(Reddit reddit, JToken json, IWebAgent webAgent) + { + CommonInit(reddit, json, webAgent); + await Task.Factory.StartNew(() => JsonConvert.PopulateObject(json["data"].ToString(), this, reddit.JsonSerializerSettings)); + SetName(); + + return this; + } + + private void SetName() + { + Name = Url.ToString(); + if (Name.StartsWith("/r/")) + Name = Name.Substring(3); + if (Name.StartsWith("r/")) + Name = Name.Substring(2); + Name = Name.TrimEnd('/'); + } + + private void CommonInit(Reddit reddit, JToken json, IWebAgent webAgent) + { + base.Init(json); + Reddit = reddit; + WebAgent = webAgent; + Wiki = new Wiki(reddit, this, webAgent); + } + + public static Subreddit GetRSlashAll(Reddit reddit) + { + var rSlashAll = new Subreddit + { + DisplayName = "/r/all", + Title = "/r/all", + Url = new Uri("/r/all", UriKind.Relative), + Name = "all", + Reddit = reddit, + WebAgent = reddit._webAgent + }; + return rSlashAll; + } + + public static Subreddit GetFrontPage(Reddit reddit) + { + var frontPage = new Subreddit + { + DisplayName = "Front Page", + Title = "reddit: the front page of the internet", + Url = new Uri("/", UriKind.Relative), + Name = "/", + Reddit = reddit, + WebAgent = reddit._webAgent + }; + return frontPage; + } + + public void Subscribe() + { + if (Reddit.User == null) + throw new AuthenticationException("No user logged in."); + var request = WebAgent.CreatePost(SubscribeUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + action = "sub", + sr = FullName, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + // Discard results + } + + public void Unsubscribe() + { + if (Reddit.User == null) + throw new AuthenticationException("No user logged in."); + var request = WebAgent.CreatePost(SubscribeUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + action = "unsub", + sr = FullName, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + // Discard results + } + + public void ClearFlairTemplates(FlairType flairType) + { + var request = WebAgent.CreatePost(ClearFlairTemplatesUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + flair_type = flairType == FlairType.Link ? "LINK_FLAIR" : "USER_FLAIR", + uh = Reddit.User.Modhash, + r = Name + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + } + + public void AddFlairTemplate(string cssClass, FlairType flairType, string text, bool userEditable) + { + var request = WebAgent.CreatePost(FlairTemplateUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + css_class = cssClass, + flair_type = flairType == FlairType.Link ? "LINK_FLAIR" : "USER_FLAIR", + text = text, + text_editable = userEditable, + uh = Reddit.User.Modhash, + r = Name, + api_type = "json" + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + } + + public string GetFlairText(string user) + { + var request = WebAgent.CreateGet(String.Format(FlairListUrl + "?name=" + user, Name)); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + return (string)json["users"][0]["flair_text"]; + } + + public string GetFlairCssClass(string user) + { + var request = WebAgent.CreateGet(String.Format(FlairListUrl + "?name=" + user, Name)); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(data); + return (string)json["users"][0]["flair_css_class"]; + } + + public void SetUserFlair(string user, string cssClass, string text) + { + var request = WebAgent.CreatePost(SetUserFlairUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + css_class = cssClass, + text = text, + uh = Reddit.User.Modhash, + r = Name, + name = user + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + } + + public void UploadHeaderImage(string name, ImageType imageType, byte[] file) + { + var request = WebAgent.CreatePost(UploadImageUrl); + var formData = new MultipartFormBuilder(request); + formData.AddDynamic(new + { + name, + uh = Reddit.User.Modhash, + r = Name, + formid = "image-upload", + img_type = imageType == ImageType.PNG ? "png" : "jpg", + upload = "", + header = 1 + }); + formData.AddFile("file", "foo.png", file, imageType == ImageType.PNG ? "image/png" : "image/jpeg"); + formData.Finish(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + // TODO: Detect errors + } + + public void AddModerator(string user) + { + var request = WebAgent.CreatePost(AddModeratorUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + uh = Reddit.User.Modhash, + r = Name, + type = "moderator", + name = user + }); + var response = request.GetResponse(); + var result = WebAgent.GetResponseString(response.GetResponseStream()); + } + + public void AcceptModeratorInvite() + { + var request = WebAgent.CreatePost(AcceptModeratorInviteUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + uh = Reddit.User.Modhash, + r = Name + }); + var response = request.GetResponse(); + var result = WebAgent.GetResponseString(response.GetResponseStream()); + } + + public void RemoveModerator(string id) + { + var request = WebAgent.CreatePost(LeaveModerationUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + uh = Reddit.User.Modhash, + r = Name, + type = "moderator", + id + }); + var response = request.GetResponse(); + var result = WebAgent.GetResponseString(response.GetResponseStream()); + } + + public override string ToString() + { + return "/r/" + DisplayName; + } + + public void AddContributor(string user) + { + var request = WebAgent.CreatePost(AddContributorUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + uh = Reddit.User.Modhash, + r = Name, + type = "contributor", + name = user + }); + var response = request.GetResponse(); + var result = WebAgent.GetResponseString(response.GetResponseStream()); + } + + public void RemoveContributor(string id) + { + var request = WebAgent.CreatePost(LeaveModerationUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + uh = Reddit.User.Modhash, + r = Name, + type = "contributor", + id + }); + var response = request.GetResponse(); + var result = WebAgent.GetResponseString(response.GetResponseStream()); + } + + public void BanUser(string user, string reason) + { + var request = WebAgent.CreatePost(BanUserUrl); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + api_type = "json", + uh = Reddit.User.Modhash, + r = Name, + type = "banned", + id = "#banned", + name = user, + note = reason, + action = "add", + container = FullName + }); + var response = request.GetResponse(); + var result = WebAgent.GetResponseString(response.GetResponseStream()); + } + + private Post Submit(SubmitData data) + { + if (Reddit.User == null) + throw new RedditException("No user logged in."); + var request = WebAgent.CreatePost(SubmitLinkUrl); + + WebAgent.WritePostBody(request.GetRequestStream(), data); + + var response = request.GetResponse(); + var result = WebAgent.GetResponseString(response.GetResponseStream()); + var json = JToken.Parse(result); + + ICaptchaSolver solver = Reddit.CaptchaSolver; + if (json["json"]["errors"].Any() && json["json"]["errors"][0][0].ToString() == "BAD_CAPTCHA" + && solver != null) + { + data.Iden = json["json"]["captcha"].ToString(); + CaptchaResponse captchaResponse = solver.HandleCaptcha(new Captcha(data.Iden)); + + // We throw exception due to this method being expected to return a valid Post object, but we cannot + // if we got a Captcha error. + if (captchaResponse.Cancel) + throw new CaptchaFailedException("Captcha verification failed when submitting " + data.Kind + " post"); + + data.Captcha = captchaResponse.Answer; + return Submit(data); + } + else if (json["json"]["errors"].Any() && json["json"]["errors"][0][0].ToString() == "ALREADY_SUB") + { + throw new DuplicateLinkException(String.Format("Post failed when submitting. The following link has already been submitted: {0}", SubmitLinkUrl)); + } + + return new Post().Init(Reddit, json["json"], WebAgent); + } + + /// + /// Submits a link post in the current subreddit using the logged-in user + /// + /// The title of the submission + /// The url of the submission link + public Post SubmitPost(string title, string url, string captchaId = "", string captchaAnswer = "", bool resubmit = false) + { + return + Submit( + new LinkData + { + Subreddit = Name, + UserHash = Reddit.User.Modhash, + Title = title, + URL = url, + Resubmit = resubmit, + Iden = captchaId, + Captcha = captchaAnswer + }); + } + + /// + /// Submits a text post in the current subreddit using the logged-in user + /// + /// The title of the submission + /// The raw markdown text of the submission + public Post SubmitTextPost(string title, string text, string captchaId = "", string captchaAnswer = "") + { + return + Submit( + new TextData + { + Subreddit = Name, + UserHash = Reddit.User.Modhash, + Title = title, + Text = text, + Iden = captchaId, + Captcha = captchaAnswer + }); + } + + #region Obsolete Getter Methods + + [Obsolete("Use Posts property instead")] + public Listing GetPosts() + { + return Posts; + } + + [Obsolete("Use New property instead")] + public Listing GetNew() + { + return New; + } + + [Obsolete("Use Hot property instead")] + public Listing GetHot() + { + return Hot; + } + + [Obsolete("Use ModQueue property instead")] + public Listing GetModQueue() + { + return ModQueue; + } + + [Obsolete("Use UnmoderatedLinks property instead")] + public Listing GetUnmoderatedLinks() + { + return UnmoderatedLinks; + } + + [Obsolete("Use Settings property instead")] + public SubredditSettings GetSettings() + { + return Settings; + } + + [Obsolete("Use UserFlairTemplates property instead")] + public UserFlairTemplate[] GetUserFlairTemplates() // Hacky, there isn't a proper endpoint for this + { + return UserFlairTemplates; + } + + [Obsolete("Use Stylesheet property instead")] + public SubredditStyle GetStylesheet() + { + return Stylesheet; + } + + [Obsolete("Use Moderators property instead")] + public IEnumerable GetModerators() + { + return Moderators; + } + + #endregion Obsolete Getter Methods + } +} diff --git a/RedditSharp/Things/Thing.cs b/RedditSharp/Things/Thing.cs new file mode 100644 index 0000000..05c1ed6 --- /dev/null +++ b/RedditSharp/Things/Thing.cs @@ -0,0 +1,113 @@ +using System; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; + +namespace RedditSharp.Things +{ + public class Thing + { + public static Thing Parse(Reddit reddit, JToken json, IWebAgent webAgent) + { + var kind = json["kind"].ValueOrDefault(); + switch (kind) + { + case "t1": + return new Comment().Init(reddit, json, webAgent, null); + case "t2": + return new RedditUser().Init(reddit, json, webAgent); + case "t3": + return new Post().Init(reddit, json, webAgent); + case "t4": + return new PrivateMessage().Init(reddit, json, webAgent); + case "t5": + return new Subreddit().Init(reddit, json, webAgent); + default: + return null; + } + } + + // if we can't determine the type of thing by "kind", try by type + public static Thing Parse(Reddit reddit, JToken json, IWebAgent webAgent) where T : Thing + { + Thing result = Parse(reddit, json, webAgent); + if (result == null) + { + if (typeof(T) == typeof(WikiPageRevision)) + { + return new WikiPageRevision().Init(reddit, json, webAgent); + } + } + return result; + } + + internal void Init(JToken json) + { + if (json == null) + return; + var data = json["name"] == null ? json["data"] : json; + FullName = data["name"].ValueOrDefault(); + Id = data["id"].ValueOrDefault(); + Kind = json["kind"].ValueOrDefault(); + FetchedAt = DateTime.Now; + } + + public virtual string Shortlink + { + get { return "http://redd.it/" + Id; } + } + + public string Id { get; set; } + public string FullName { get; set; } + public string Kind { get; set; } + + /// + /// The time at which this object was fetched from reddit servers. + /// + public DateTime FetchedAt { get; private set; } + + /// + /// Gets the time since last fetch from reddit servers. + /// + public TimeSpan TimeSinceFetch + { + get + { + return DateTime.Now - FetchedAt; + } + } + + public static async Task ParseAsync(Reddit reddit, JToken json, IWebAgent webAgent) + { + var kind = json["kind"].ValueOrDefault(); + switch (kind) + { + case "t1": + return await new Comment().InitAsync(reddit, json, webAgent, null); + case "t2": + return await new RedditUser().InitAsync(reddit, json, webAgent); + case "t3": + return await new Post().InitAsync(reddit, json, webAgent); + case "t4": + return await new PrivateMessage().InitAsync(reddit, json, webAgent); + case "t5": + return await new Subreddit().InitAsync(reddit, json, webAgent); + default: + return null; + } + } + + // if we can't determine the type of thing by "kind", try by type + public static async Task ParseAsync(Reddit reddit, JToken json, IWebAgent webAgent) where T : Thing + { + Thing result = await ParseAsync(reddit, json, webAgent); + if (result == null) + { + if (typeof(T) == typeof(WikiPageRevision)) + { + return await new WikiPageRevision().InitAsync(reddit, json, webAgent); + } + } + return result; + } + } +} diff --git a/RedditSharp/Things/VotableThing.cs b/RedditSharp/Things/VotableThing.cs new file mode 100644 index 0000000..c7aa188 --- /dev/null +++ b/RedditSharp/Things/VotableThing.cs @@ -0,0 +1,162 @@ +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp.Things +{ + public class VotableThing : CreatedThing + { + public enum VoteType + { + Upvote = 1, + None = 0, + Downvote = -1 + } + + private const string VoteUrl = "/api/vote"; + private const string SaveUrl = "/api/save"; + private const string UnsaveUrl = "/api/unsave"; + + [JsonIgnore] + private IWebAgent WebAgent { get; set; } + + [JsonIgnore] + private Reddit Reddit { get; set; } + + protected VotableThing Init(Reddit reddit, IWebAgent webAgent, JToken json) + { + CommonInit(reddit, webAgent, json); + JsonConvert.PopulateObject(json["data"].ToString(), this, Reddit.JsonSerializerSettings); + return this; + } + protected async Task InitAsync(Reddit reddit, IWebAgent webAgent, JToken json) + { + CommonInit(reddit, webAgent, json); + await Task.Factory.StartNew(() => JsonConvert.PopulateObject(json["data"].ToString(), this, Reddit.JsonSerializerSettings)); + return this; + } + + private void CommonInit(Reddit reddit, IWebAgent webAgent, JToken json) + { + base.Init(reddit, json); + Reddit = reddit; + WebAgent = webAgent; + } + + [JsonProperty("downs")] + public int Downvotes { get; set; } + [JsonProperty("ups")] + public int Upvotes { get; set; } + [JsonProperty("saved")] + public bool Saved { get; set; } + + /// + /// True if the logged in user has upvoted this. + /// False if they have not. + /// Null if they have not cast a vote. + /// + [JsonProperty("likes")] + public bool? Liked { get; set; } + + /// + /// Gets or sets the vote for the current VotableThing. + /// + [JsonIgnore] + public VoteType Vote + { + get + { + switch (this.Liked) + { + case true: return VoteType.Upvote; + case false: return VoteType.Downvote; + + default: return VoteType.None; + } + } + set { this.SetVote(value); } + } + + public void Upvote() + { + this.SetVote(VoteType.Upvote); + } + + public void Downvote() + { + this.SetVote(VoteType.Downvote); + } + + public void SetVote(VoteType type) + { + if (this.Vote == type) return; + + var request = WebAgent.CreatePost(VoteUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + dir = (int)type, + id = FullName, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + + if (Liked == true) Upvotes--; + if (Liked == false) Downvotes--; + + switch(type) + { + case VoteType.Upvote: Liked = true; Upvotes++; return; + case VoteType.None: Liked = null; return; + case VoteType.Downvote: Liked = false; Downvotes++; return; + } + } + + public void Save() + { + var request = WebAgent.CreatePost(SaveUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + id = FullName, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + Saved = true; + } + + public void Unsave() + { + var request = WebAgent.CreatePost(UnsaveUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + id = FullName, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + Saved = false; + } + + public void ClearVote() + { + var request = WebAgent.CreatePost(VoteUrl); + var stream = request.GetRequestStream(); + WebAgent.WritePostBody(stream, new + { + dir = 0, + id = FullName, + uh = Reddit.User.Modhash + }); + stream.Close(); + var response = request.GetResponse(); + var data = WebAgent.GetResponseString(response.GetResponseStream()); + } + } +} diff --git a/RedditSharp/Things/WikiPageRevision.cs b/RedditSharp/Things/WikiPageRevision.cs new file mode 100644 index 0000000..31c08ed --- /dev/null +++ b/RedditSharp/Things/WikiPageRevision.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RedditSharp.Things +{ + public class WikiPageRevision : Thing + { + [JsonProperty("id")] + new public string Id { get; private set; } + + [JsonProperty("timestamp")] + [JsonConverter(typeof(UnixTimestampConverter))] + public DateTime? TimeStamp { get; set; } + + [JsonProperty("reason")] + public string Reason { get; private set; } + + [JsonProperty("page")] + public string Page { get; private set; } + + [JsonIgnore] + public RedditUser Author { get; set; } + + protected internal WikiPageRevision() { } + + internal WikiPageRevision Init(Reddit reddit, JToken json, IWebAgent webAgent) + { + CommonInit(reddit, json, webAgent); + JsonConvert.PopulateObject(json.ToString(), this, reddit.JsonSerializerSettings); + return this; + } + + internal async Task InitAsync(Reddit reddit, JToken json, IWebAgent webAgent) + { + CommonInit(reddit, json, webAgent); + await Task.Factory.StartNew(() => JsonConvert.PopulateObject(json.ToString(), this, reddit.JsonSerializerSettings)); + return this; + } + + private void CommonInit(Reddit reddit, JToken json, IWebAgent webAgent) + { + base.Init(json); + Author = new RedditUser().Init(reddit, json["author"], webAgent); + } + } +} \ No newline at end of file diff --git a/RedditSharp/UnixTimeStamp.cs b/RedditSharp/UnixTimeStamp.cs new file mode 100644 index 0000000..be2777e --- /dev/null +++ b/RedditSharp/UnixTimeStamp.cs @@ -0,0 +1,15 @@ +using System; + +namespace RedditSharp +{ + public static class UnixTimeStamp + { + public static DateTime UnixTimeStampToDateTime(this long unixTimeStamp) + { + // Unix timestamp is seconds past epoch + var dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + dtDateTime = dtDateTime.AddSeconds(unixTimeStamp); + return dtDateTime; + } + } +} diff --git a/RedditSharp/UnixTimestampConverter.cs b/RedditSharp/UnixTimestampConverter.cs new file mode 100644 index 0000000..21cd5f5 --- /dev/null +++ b/RedditSharp/UnixTimestampConverter.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +namespace RedditSharp +{ + public class UnixTimestampConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(double) || objectType == typeof(DateTime); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + return token.Value().UnixTimeStampToDateTime(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value); + } + } +} diff --git a/RedditSharp/UrlParser.cs b/RedditSharp/UrlParser.cs new file mode 100644 index 0000000..c2d4576 --- /dev/null +++ b/RedditSharp/UrlParser.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +namespace RedditSharp +{ + class UrlParser : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(String) || objectType == typeof(Uri); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + + if (token.Type == JTokenType.String) + { + if (Type.GetType("Mono.Runtime") == null) + return new Uri(token.Value(), UriKind.RelativeOrAbsolute); + if (token.Value().StartsWith("/")) + return new Uri(token.Value(), UriKind.Relative); + return new Uri(token.Value(), UriKind.RelativeOrAbsolute); + } + else + return token.Value(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value); + } + } +} diff --git a/RedditSharp/WebAgent.cs b/RedditSharp/WebAgent.cs new file mode 100644 index 0000000..86859ce --- /dev/null +++ b/RedditSharp/WebAgent.cs @@ -0,0 +1,255 @@ +using System; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Web; +using Newtonsoft.Json.Linq; + +namespace RedditSharp +{ + public sealed class WebAgent : IWebAgent + { + /// + /// Additional values to append to the default RedditSharp user agent. + /// + public static string UserAgent { get; set; } + + /// + /// It is strongly advised that you leave this enabled. Reddit bans excessive + /// requests with extreme predjudice. + /// + public static bool EnableRateLimit { get; set; } + + public static string Protocol { get; set; } + + /// + /// It is strongly advised that you leave this set to Burst or Pace. Reddit bans excessive + /// requests with extreme predjudice. + /// + public static RateLimitMode RateLimit { get; set; } + + /// + /// The method by which the WebAgent will limit request rate + /// + public enum RateLimitMode + { + /// + /// Limits requests to one every two seconds + /// + Pace, + /// + /// Restricts requests to thirty per minute + /// + Burst, + /// + /// Does not restrict request rate. ***NOT RECOMMENDED*** + /// + None + } + + /// + /// The root domain RedditSharp uses to address Reddit. + /// www.reddit.com by default + /// + public static string RootDomain { get; set; } + + /// + /// Used to make calls against Reddit's API using OAuth23 + /// + public string AccessToken { get; set; } + + public CookieContainer Cookies { get; set; } + public string AuthCookie { get; set; } + + private static DateTime _lastRequest; + private static DateTime _burstStart; + private static int _requestsThisBurst; + + public JToken CreateAndExecuteRequest(string url) + { + Uri uri; + if (!Uri.TryCreate(url, UriKind.Absolute, out uri)) + { + if (!Uri.TryCreate(String.Format("{0}://{1}{2}", Protocol, RootDomain, url), UriKind.Absolute, out uri)) + throw new Exception("Could not parse Uri"); + } + var request = CreateGet(uri); + try { return ExecuteRequest(request); } + catch (Exception) + { + var tempProtocol = Protocol; + var tempRootDomain = RootDomain; + Protocol = "http"; + RootDomain = "www.reddit.com"; + var retval = CreateAndExecuteRequest(url); + Protocol = tempProtocol; + RootDomain = tempRootDomain; + return retval; + } + } + + /// + /// Executes the web request and handles errors in the response + /// + /// + /// + public JToken ExecuteRequest(HttpWebRequest request) + { + EnforceRateLimit(); + var response = request.GetResponse(); + var result = GetResponseString(response.GetResponseStream()); + + var json = JToken.Parse(result); + try + { + if (json["json"] != null) + { + json = json["json"]; //get json object if there is a root node + } + if (json["error"] != null) + { + switch (json["error"].ToString()) + { + case "404": + throw new Exception("File Not Found"); + case "403": + throw new Exception("Restricted"); + case "invalid_grant": + //Refresh authtoken + //AccessToken = authProvider.GetRefreshToken(); + //ExecuteRequest(request); + break; + } + } + } + catch + { + } + return json; + + } + + private static void EnforceRateLimit() + { + switch (RateLimit) + { + case RateLimitMode.Pace: + while ((DateTime.UtcNow - _lastRequest).TotalSeconds < 2)// Rate limiting + Thread.Sleep(250); + _lastRequest = DateTime.UtcNow; + break; + case RateLimitMode.Burst: + if (_requestsThisBurst == 0)//this is first request + _burstStart = DateTime.UtcNow; + if (_requestsThisBurst >= 30) //limit has been reached + { + while ((DateTime.UtcNow - _burstStart).TotalSeconds < 60) + Thread.Sleep(250); + _burstStart = DateTime.UtcNow; + } + _requestsThisBurst++; + break; + } + } + + public HttpWebRequest CreateRequest(string url, string method) + { + EnforceRateLimit(); + bool prependDomain; + // IsWellFormedUriString returns true on Mono for some reason when using a string like "/api/me" + if (Type.GetType("Mono.Runtime") != null) + prependDomain = !url.StartsWith("http://") && !url.StartsWith("https://"); + else + prependDomain = !Uri.IsWellFormedUriString(url, UriKind.Absolute); + + HttpWebRequest request; + if (prependDomain) + request = (HttpWebRequest)WebRequest.Create(String.Format("{0}://{1}{2}", Protocol, RootDomain, url)); + else + request = (HttpWebRequest)WebRequest.Create(url); + request.CookieContainer = Cookies; + if (Type.GetType("Mono.Runtime") != null) + { + var cookieHeader = Cookies.GetCookieHeader(new Uri("http://reddit.com")); + request.Headers.Set("Cookie", cookieHeader); + } + if (RootDomain == "oauth.reddit.com")// use OAuth + { + request.Headers.Set("Authorization", "bearer " + AccessToken);//Must be included in OAuth calls + } + request.Method = method; + request.UserAgent = UserAgent + " - with RedditSharp by /u/sircmpwn"; + return request; + } + + private HttpWebRequest CreateRequest(Uri uri, string method) + { + EnforceRateLimit(); + var request = (HttpWebRequest)WebRequest.Create(uri); + request.CookieContainer = Cookies; + if (Type.GetType("Mono.Runtime") != null) + { + var cookieHeader = Cookies.GetCookieHeader(new Uri("http://reddit.com")); + request.Headers.Set("Cookie", cookieHeader); + } + if (RootDomain == "oauth.reddit.com")// use OAuth + { + request.Headers.Set("Authorization", "bearer " + AccessToken);//Must be included in OAuth calls + } + request.Method = method; + request.UserAgent = UserAgent + " - with RedditSharp by /u/sircmpwn"; + return request; + } + + public HttpWebRequest CreateGet(string url) + { + return CreateRequest(url, "GET"); + } + + private HttpWebRequest CreateGet(Uri url) + { + return CreateRequest(url, "GET"); + } + + public HttpWebRequest CreatePost(string url) + { + var request = CreateRequest(url, "POST"); + request.ContentType = "application/x-www-form-urlencoded"; + return request; + } + + public string GetResponseString(Stream stream) + { + var data = new StreamReader(stream).ReadToEnd(); + stream.Close(); + return data; + } + + public void WritePostBody(Stream stream, object data, params string[] additionalFields) + { + var type = data.GetType(); + var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + string value = ""; + foreach (var property in properties) + { + var attr = property.GetCustomAttributes(typeof(RedditAPINameAttribute), false).FirstOrDefault() as RedditAPINameAttribute; + string name = attr == null ? property.Name : attr.Name; + var entry = Convert.ToString(property.GetValue(data, null)); + value += name + "=" + HttpUtility.UrlEncode(entry).Replace(";", "%3B").Replace("&", "%26") + "&"; + } + for (int i = 0; i < additionalFields.Length; i += 2) + { + var entry = Convert.ToString(additionalFields[i + 1]) ?? string.Empty; + value += additionalFields[i] + "=" + HttpUtility.UrlEncode(entry).Replace(";", "%3B").Replace("&", "%26") + "&"; + } + value = value.Remove(value.Length - 1); // Remove trailing & + var raw = Encoding.UTF8.GetBytes(value); + stream.Write(raw, 0, raw.Length); + stream.Close(); + } + } +} diff --git a/RedditSharp/Wiki.cs b/RedditSharp/Wiki.cs new file mode 100644 index 0000000..5ddb429 --- /dev/null +++ b/RedditSharp/Wiki.cs @@ -0,0 +1,168 @@ +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Linq; +using RedditSharp.Things; + +namespace RedditSharp +{ + using System; + + public class Wiki + { + private Reddit Reddit { get; set; } + private Subreddit Subreddit { get; set; } + private IWebAgent WebAgent { get; set; } + + private const string GetWikiPageUrl = "/r/{0}/wiki/{1}.json?v={2}"; + private const string GetWikiPagesUrl = "/r/{0}/wiki/pages.json"; + private const string WikiPageEditUrl = "/r/{0}/api/wiki/edit"; + private const string HideWikiPageUrl = "/r/{0}/api/wiki/hide"; + private const string RevertWikiPageUrl = "/r/{0}/api/wiki/revert"; + private const string WikiPageAllowEditorAddUrl = "/r/{0}/api/wiki/alloweditor/add"; + private const string WikiPageAllowEditorDelUrl = "/r/{0}/api/wiki/alloweditor/del"; + private const string WikiPageSettingsUrl = "/r/{0}/wiki/settings/{1}.json"; + private const string WikiRevisionsUrl = "/r/{0}/wiki/revisions.json"; + private const string WikiPageRevisionsUrl = "/r/{0}/wiki/revisions/{1}.json"; + private const string WikiPageDiscussionsUrl = "/r/{0}/wiki/discussions/{1}.json"; + + public IEnumerable PageNames + { + get + { + var request = WebAgent.CreateGet(string.Format(GetWikiPagesUrl, Subreddit.Name)); + var response = request.GetResponse(); + string json = WebAgent.GetResponseString(response.GetResponseStream()); + return JObject.Parse(json)["data"].Values(); + } + } + + public Listing Revisions + { + get + { + return new Listing(Reddit, string.Format(WikiRevisionsUrl, Subreddit.Name), WebAgent); + } + } + + protected internal Wiki(Reddit reddit, Subreddit subreddit, IWebAgent webAgent) + { + Reddit = reddit; + Subreddit = subreddit; + WebAgent = webAgent; + } + + public WikiPage GetPage(string page, string version = null) + { + var request = WebAgent.CreateGet(string.Format(GetWikiPageUrl, Subreddit.Name, page, version)); + var response = request.GetResponse(); + var json = JObject.Parse(WebAgent.GetResponseString(response.GetResponseStream())); + var result = new WikiPage(Reddit, json["data"], WebAgent); + return result; + } + + #region Settings + public WikiPageSettings GetPageSettings(string name) + { + var request = WebAgent.CreateGet(string.Format(WikiPageSettingsUrl, Subreddit.Name, name)); + var response = request.GetResponse(); + var json = JObject.Parse(WebAgent.GetResponseString(response.GetResponseStream())); + var result = new WikiPageSettings(Reddit, json["data"], WebAgent); + return result; + } + + public void SetPageSettings(string name, WikiPageSettings settings) + { + var request = WebAgent.CreatePost(string.Format(WikiPageSettingsUrl, Subreddit.Name, name)); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + page = name, + permlevel = settings.PermLevel, + listed = settings.Listed, + uh = Reddit.User.Modhash + }); + var response = request.GetResponse(); + } + #endregion + + #region Revisions + + public Listing GetPageRevisions(string page) + { + return new Listing(Reddit, string.Format(WikiPageRevisionsUrl, Subreddit.Name, page), WebAgent); + } + #endregion + + #region Discussions + public Listing GetPageDiscussions(string page) + { + return new Listing(Reddit, string.Format(WikiPageDiscussionsUrl, Subreddit.Name, page), WebAgent); + } + #endregion + + public void EditPage(string page, string content, string previous = null, string reason = null) + { + var request = WebAgent.CreatePost(string.Format(WikiPageEditUrl, Subreddit.Name)); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + content = content, + previous = previous, + reason = reason, + page = page, + uh = Reddit.User.Modhash + }); + var response = request.GetResponse(); + } + + public void HidePage(string page, string revision) + { + var request = WebAgent.CreatePost(string.Format(HideWikiPageUrl, Subreddit.Name)); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + page = page, + revision = revision, + uh = Reddit.User.Modhash + }); + var response = request.GetResponse(); + } + + public void RevertPage(string page, string revision) + { + var request = WebAgent.CreatePost(string.Format(RevertWikiPageUrl, Subreddit.Name)); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + page = page, + revision = revision, + uh = Reddit.User.Modhash + }); + var response = request.GetResponse(); + } + + public void SetPageEditor(string page, string username, bool allow) + { + var request = WebAgent.CreatePost(string.Format(allow ? WikiPageAllowEditorAddUrl : WikiPageAllowEditorDelUrl, Subreddit.Name)); + WebAgent.WritePostBody(request.GetRequestStream(), new + { + page = page, + username = username, + uh = Reddit.User.Modhash + }); + var response = request.GetResponse(); + } + + #region Obsolete Getter Methods + + [Obsolete("Use PageNames property instead")] + public IEnumerable GetPageNames() + { + return PageNames; + } + + [Obsolete("Use Revisions property instead")] + public Listing GetRevisions() + { + return Revisions; + } + + #endregion Obsolete Getter Methods + } +} \ No newline at end of file diff --git a/RedditSharp/WikiPage.cs b/RedditSharp/WikiPage.cs new file mode 100644 index 0000000..3c8e917 --- /dev/null +++ b/RedditSharp/WikiPage.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using RedditSharp.Things; + +namespace RedditSharp +{ + public class WikiPage + { + [JsonProperty("may_revise")] + public string MayRevise { get; set; } + + [JsonProperty("revision_date")] + [JsonConverter(typeof(UnixTimestampConverter))] + public DateTime? RevisionDate { get; set; } + + [JsonProperty("content_html")] + public string HtmlContent { get; set; } + + [JsonProperty("content_md")] + public string MarkdownContent { get; set; } + + [JsonIgnore] + public RedditUser RevisionBy { get; set; } + + protected internal WikiPage(Reddit reddit, JToken json, IWebAgent webAgent) + { + RevisionBy = new RedditUser().Init(reddit, json["revision_by"], webAgent); + JsonConvert.PopulateObject(json.ToString(), this, reddit.JsonSerializerSettings); + } + } +} \ No newline at end of file diff --git a/RedditSharp/WikiPageSettings.cs b/RedditSharp/WikiPageSettings.cs new file mode 100644 index 0000000..41cf35e --- /dev/null +++ b/RedditSharp/WikiPageSettings.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Linq; +using RedditSharp.Things; + +namespace RedditSharp +{ + public class WikiPageSettings + { + [JsonProperty("listed")] + public bool Listed { get; set; } + + [JsonProperty("permlevel")] + public int PermLevel { get; set; } + + [JsonIgnore] + public IEnumerable Editors { get; set; } + + public WikiPageSettings() + { + } + + protected internal WikiPageSettings(Reddit reddit, JToken json, IWebAgent webAgent) + { + var editors = json["editors"].ToArray(); + Editors = editors.Select(x => new RedditUser().Init(reddit, x, webAgent)); + JsonConvert.PopulateObject(json.ToString(), this, reddit.JsonSerializerSettings); + } + } +} \ No newline at end of file