Skip to content

Commit 730bd7b

Browse files
committed
Add support for collections
1 parent 9a36496 commit 730bd7b

File tree

8 files changed

+348
-0
lines changed

8 files changed

+348
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace RedditSharp.Data.Collections
2+
{
3+
internal class AddOrRemovePostsFromCollectionParams
4+
{
5+
/// <summary>
6+
/// UUID of a collection
7+
/// </summary>
8+
[RedditAPIName("collection_id")]
9+
internal string CollectionId { get; set; }
10+
11+
/// <summary>
12+
/// Full name of link, e.g. t3_xyz
13+
/// </summary>
14+
[RedditAPIName("link_fullname")]
15+
internal string LinkFullName { get; set; }
16+
}
17+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace RedditSharp.Data.Collections
2+
{
3+
internal class CollectionCreationParams
4+
{
5+
/// <summary>
6+
/// Title of the submission. Maximum 300 characters.
7+
/// </summary>
8+
[RedditAPIName("title")]
9+
internal string Title { get; set; }
10+
11+
/// <summary>
12+
/// Description of the collection. Maximum of 500 characters.
13+
/// </summary>
14+
[RedditAPIName("description")]
15+
internal string Description { get; set; }
16+
17+
/// <summary>
18+
/// Name of the subreddit to which you are submitting.
19+
/// </summary>
20+
[RedditAPIName("sr_fullname")]
21+
internal string Subreddit { get; set; }
22+
}
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace RedditSharp.Data.Collections
2+
{
3+
internal class CollectionBaseParams
4+
{
5+
/// <summary>
6+
/// UUID of a collection
7+
/// </summary>
8+
[RedditAPIName("collection_id")]
9+
internal string CollectionId { get; set; }
10+
}
11+
12+
internal class GetCollectionParams : CollectionBaseParams
13+
{
14+
/// <summary>
15+
/// Should include all the links
16+
/// </summary>
17+
[RedditAPIName("include_links")]
18+
internal bool IncludeLinks { get; set; }
19+
}
20+
}

RedditSharp/Data/Urls.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace RedditSharp.Data
2+
{
3+
internal static class Urls
4+
{
5+
internal static class Collections
6+
{
7+
internal const string AddPost = "/api/v1/collections/add_post_to_collection";
8+
internal static string Get(string collectionId, bool includeLinks) => $"/api/v1/collections/collection.json?collection_id={collectionId}&include_links={includeLinks}";
9+
internal const string CreateCollectionUrl = "/api/v1/collections/create_collection";
10+
internal const string Delete = "/api/v1/collections/delete_collection";
11+
internal const string RemovePost = "/api/v1/collections/remove_post_in_collection";
12+
internal static string SubredditCollectionsUrl(string fullName) => $"/api/v1/collections/subreddit_collections.json?sr_fullname={fullName}";
13+
}
14+
}
15+
}

RedditSharp/Reddit.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
using System.Linq.Expressions;
66
using System.Security.Authentication;
77
using System.Threading.Tasks;
8+
using RedditSharp.Data;
9+
using RedditSharp.Data.Collections;
10+
using RedditSharp.Extensions.JTokenExtensions;
811
using DefaultWebAgent = RedditSharp.WebAgent;
912

1013
namespace RedditSharp
@@ -477,5 +480,23 @@ public Listing<T> GetListing<T>(string url, int maxLimit = -1, int limitPerReque
477480
{
478481
return new Listing<T>(this.WebAgent, url, maxLimit, limitPerRequest);
479482
}
483+
484+
public async Task<Collection> GetCollectionAsync(string collectionId, bool includePostsContent = true)
485+
{
486+
var json = await WebAgent.Get(Urls.Collections.Get(collectionId, includePostsContent));
487+
json.ThrowIfHasErrors("Could not retrieve the collection.");
488+
return new Collection(json, WebAgent);
489+
}
490+
491+
/// <summary>
492+
/// Deletes the specified collection. Must be a mod of the subreddit to delete.
493+
/// </summary>
494+
/// <param name="collectionId"></param>
495+
/// <returns></returns>
496+
public async Task DeleteCollectionAsync(string collectionId)
497+
{
498+
var json = await WebAgent.Post(Urls.Collections.Delete, new CollectionBaseParams { CollectionId = collectionId });
499+
json.ThrowIfHasErrors("Could not delete the collection.");
500+
}
480501
}
481502
}

RedditSharp/Things/Collection.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Newtonsoft.Json;
6+
using Newtonsoft.Json.Linq;
7+
using RedditSharp.Data;
8+
using RedditSharp.Data.Collections;
9+
using RedditSharp.Extensions.JTokenExtensions;
10+
using RedditSharp.Interfaces;
11+
12+
namespace RedditSharp.Things
13+
{
14+
public class Collection : ISettableWebAgent
15+
{
16+
[JsonProperty("subreddit_id")]
17+
public string SubredditId { get; internal set; }
18+
19+
[JsonProperty("description")]
20+
public string Description { get; internal set; }
21+
22+
[JsonProperty("author_name")]
23+
public string AuthorName { get; internal set; }
24+
25+
[JsonProperty("collection_id")]
26+
public string CollectionId { get; internal set; }
27+
28+
[JsonProperty("display_layout")]
29+
public string DisplayLayout { get; internal set; }
30+
31+
[JsonProperty("permalink")]
32+
public string Permalink { get; internal set; }
33+
34+
[JsonProperty("link_ids")]
35+
public string[] LinkIds { get; internal set; }
36+
37+
[JsonProperty("title")]
38+
public string Title { get; internal set; }
39+
40+
[JsonProperty("created_at_utc"), JsonConverter(typeof(UnixTimestampConverter))]
41+
public DateTime CreatedAtUtc { get; internal set; }
42+
43+
[JsonProperty("author_id")]
44+
public string AuthorId { get; internal set; }
45+
46+
[JsonProperty("last_update_utc"), JsonConverter(typeof(UnixTimestampConverter))]
47+
public DateTime LastUpdateUtc { get; internal set; }
48+
49+
public Post[] Posts { get; }
50+
51+
public IWebAgent WebAgent { private get; set; }
52+
53+
public Collection()
54+
{
55+
}
56+
57+
public Collection(JToken json, IWebAgent agent)
58+
{
59+
WebAgent = agent;
60+
61+
Helpers.PopulateObject(json, this);
62+
63+
var posts = new List<Post>();
64+
var children = json.SelectToken("sorted_links.data.children");
65+
if (children != null && children.Type == JTokenType.Array)
66+
{
67+
posts.AddRange(children.Select(item => new Post(WebAgent, item)));
68+
}
69+
70+
Posts = posts.ToArray();
71+
}
72+
73+
/// <summary>
74+
/// Adds a post to the collection
75+
/// </summary>
76+
/// <param name="linkFullName">Full name of link, e.g. t3_xyz</param>
77+
public async Task AddPostAsync(string linkFullName)
78+
{
79+
var data = new AddOrRemovePostsFromCollectionParams
80+
{
81+
CollectionId = CollectionId,
82+
LinkFullName = linkFullName,
83+
};
84+
var json = await WebAgent.Post(Urls.Collections.AddPost, data);
85+
json.ThrowIfHasErrors("Could not add post to collection.");
86+
}
87+
88+
/// <summary>
89+
/// Removes a post from the collection
90+
/// </summary>
91+
/// <param name="linkFullName">Full name of link, e.g. t3_xyz</param>
92+
public async Task RemovePostAsync(string linkFullName)
93+
{
94+
var data = new AddOrRemovePostsFromCollectionParams
95+
{
96+
CollectionId = CollectionId,
97+
LinkFullName = linkFullName,
98+
};
99+
var json = await WebAgent.Post(Urls.Collections.RemovePost, data);
100+
json.ThrowIfHasErrors("Could not remove post from collection.");
101+
}
102+
103+
public async Task DeleteAsync()
104+
{
105+
var json = await WebAgent.Post(Urls.Collections.Delete, null);
106+
json.ThrowIfHasErrors("Could not remove collection.");
107+
}
108+
}
109+
}

RedditSharp/Things/Subreddit.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
using System.Linq;
77
using System.Text.RegularExpressions;
88
using System.Threading.Tasks;
9+
using RedditSharp.Data;
10+
using RedditSharp.Data.Collections;
11+
using RedditSharp.Extensions.JTokenExtensions;
912

1013
namespace RedditSharp.Things
1114
{
@@ -936,6 +939,28 @@ public Listing<ModAction> GetModerationLog(ModActionType action, IEnumerable<str
936939
return Listing<ModAction>.Create(WebAgent, url, max, 500);
937940
}
938941

942+
public async Task<Collection> CreateCollectionAsync(string title, string description)
943+
{
944+
var data = new CollectionCreationParams
945+
{
946+
Subreddit = FullName,
947+
Title = title,
948+
Description = description,
949+
};
950+
var json = await WebAgent.Post(Urls.Collections.CreateCollectionUrl, data).ConfigureAwait(false);
951+
json.ThrowIfHasErrors("Could not create collection.");
952+
var result = new Collection(json, WebAgent);
953+
return result;
954+
}
955+
956+
public async Task<List<Collection>> GetCollectionsAsync()
957+
{
958+
var json = await WebAgent.Get(Urls.Collections.SubredditCollectionsUrl(FullName)).ConfigureAwait(false);
959+
json.ThrowIfHasErrors("Could not retrieve collections.");
960+
var result = Helpers.PopulateObjects<Collection>(json, WebAgent);
961+
return result;
962+
}
963+
939964
#region Static Operations
940965

941966
public static async Task<IEnumerable<ModeratorUser>> GetModeratorsAsync(IWebAgent agent, string subreddit ) {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using RedditSharp;
5+
using RedditSharp.Things;
6+
using Retry;
7+
using Xunit;
8+
9+
namespace RedditSharpTests.Collections
10+
{
11+
[Collection("AuthenticatedTests")]
12+
public class CollectionTests
13+
{
14+
private readonly Reddit _reddit;
15+
private readonly string _subredditName;
16+
17+
public CollectionTests(AuthenticatedTestsFixture authenticatedFixture)
18+
{
19+
var authFixture = authenticatedFixture;
20+
var agent = new WebAgent(authFixture.AccessToken);
21+
_reddit = new Reddit(agent, true);
22+
_subredditName = authFixture.Config["TestSubreddit"];
23+
}
24+
25+
[SkippableFact]
26+
public async Task CreatingACollection()
27+
{
28+
var guid = GenerateGuid();
29+
var currentDate = DateTime.UtcNow.AddMinutes(-2);
30+
31+
var sub = await _reddit.GetSubredditAsync(_subredditName);
32+
SkipIfNotModerator(sub);
33+
34+
var title = $"A collection with no posts {guid}";
35+
var description = $"Collection description {GenerateGuid()}";
36+
37+
var result = await sub.CreateCollectionAsync(title, description);
38+
39+
Assert.Equal(description, result.Description);
40+
Assert.Equal(title, result.Title);
41+
Assert.Equal(sub.FullName, result.SubredditId);
42+
Assert.True(result.CreatedAtUtc >= currentDate);
43+
Assert.True(result.LastUpdateUtc >= currentDate);
44+
45+
var collections = await sub.GetCollectionsAsync();
46+
Assert.True(collections.Count >= 1, "there should be at least one collection");
47+
var collection = collections.FirstOrDefault(x => x.CollectionId == result.CollectionId);
48+
Assert.NotNull(collection);
49+
50+
await _reddit.DeleteCollectionAsync(collection.CollectionId);
51+
}
52+
53+
[SkippableFact]
54+
public async Task CreatingACollectionAndAddingPosts()
55+
{
56+
var post1Guid = GenerateGuid();
57+
var post2Guid = GenerateGuid();
58+
var title = $"Collection of {post1Guid} and {post2Guid}";
59+
var description = $"Awesome new collection {GenerateGuid()}";
60+
61+
var sub = await _reddit.GetSubredditAsync(_subredditName);
62+
SkipIfNotModerator(sub);
63+
64+
var post1Task = sub.SubmitPostAsync($"Post {post1Guid}", "https://github.com/CrustyJew/RedditSharp", resubmit: true);
65+
var post2Task = sub.SubmitTextPostAsync($"Post {post2Guid}", $"Post {post2Guid}");
66+
67+
var createCollectionTask = sub.CreateCollectionAsync(title, description);
68+
69+
var post1 = await post1Task;
70+
var post2 = await post2Task;
71+
var collectionResult = await createCollectionTask;
72+
73+
Assert.NotNull(post1);
74+
Assert.NotNull(post2);
75+
76+
var addPost1Task = collectionResult.AddPostAsync(post1.FullName);
77+
var addPost2Task = collectionResult.AddPostAsync(post2.FullName);
78+
79+
await addPost1Task;
80+
await addPost2Task;
81+
82+
var collection = await RetryHelper.Instance
83+
.Try(() => _reddit.GetCollectionAsync(collectionResult.CollectionId))
84+
.WithTryInterval(TimeSpan.FromSeconds(0.5))
85+
.WithMaxTryCount(10)
86+
.Until(c => c.LinkIds.Length > 1);
87+
88+
Assert.Equal(2, collection.LinkIds.Length);
89+
Assert.Contains(post1.FullName, collection.LinkIds);
90+
Assert.Contains(post2.FullName, collection.LinkIds);
91+
92+
Assert.Equal(2, collection.Posts.Length);
93+
94+
var collectionWithLinkContent = await _reddit.GetCollectionAsync(collectionResult.CollectionId, includePostsContent: false);
95+
96+
Assert.Empty(collectionWithLinkContent.Posts);
97+
98+
await _reddit.DeleteCollectionAsync(collection.CollectionId);
99+
}
100+
101+
[Fact]
102+
public async Task DeletingANonExistentCollection()
103+
{
104+
var exception = await Assert.ThrowsAsync<RedditException>(() => _reddit.DeleteCollectionAsync("00000000-0000-0000-1111-111111111111"));
105+
Assert.Contains(exception.Errors, error => error[0].ToString().Equals("INVALID_COLLECTION_ID"));
106+
}
107+
108+
private void SkipIfNotModerator(Subreddit sub)
109+
{
110+
Skip.If(sub.UserIsModerator != true, $"User isn't a moderator of ${_subredditName} so a collection cannot be made.");
111+
}
112+
113+
private static string GenerateGuid()
114+
{
115+
return Guid.NewGuid().ToString("N").Substring(0, 5);
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)