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