(response.Content);
30 |
31 | throw StreamException.FromResponse(response);
32 | }
33 |
34 | public async Task DeleteAsync(string url)
35 | {
36 | var request = _client.BuildAppRequest("files/", HttpMethod.Delete);
37 | request.AddQueryParameter("url", url);
38 |
39 | var response = await _client.MakeRequestAsync(request);
40 |
41 | if (response.StatusCode != HttpStatusCode.OK)
42 | throw StreamException.FromResponse(response);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/samples/ConsoleApp/README.md:
--------------------------------------------------------------------------------
1 | # ConsoleApp sample
2 |
3 |
4 |
5 |
6 |
7 | An example simple console app of how can you use our .NET SDK.
8 |
9 | Explore the docs »
10 |
11 |
12 | ## 📝 Overview
13 |
14 | To try out this project, open up a terminal and run the following:
15 |
16 |
17 | ```shell
18 | $ dotnet run
19 | ```
20 |
21 | ## ✍️ Contributing
22 |
23 | We welcome code changes that improve this library or fix a problem, please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github. We are very happy to merge your code in the official repository. Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first. See our [license file](../../LICENSE) for more details.
24 |
25 | Head over to [CONTRIBUTING.md](../../CONTRIBUTING.md) for some development tips.
26 |
27 | ## 🧑💻 We are hiring!
28 |
29 | We've recently closed a [$38 million Series B funding round](https://techcrunch.com/2021/03/04/stream-raises-38m-as-its-chat-and-activity-feed-apis-power-communications-for-1b-users/) and we keep actively growing.
30 | Our APIs are used by more than a billion end-users, and you'll have a chance to make a huge impact on the product within a team of the strongest engineers all over the world.
31 |
32 | Check out our current openings and apply via [Stream's website](https://getstream.io/team/#jobs).
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | pull_request:
5 | types: [closed]
6 | branches:
7 | - master
8 |
9 | jobs:
10 | Release:
11 | if: github.event.pull_request.merged && startsWith(github.head_ref, 'release-')
12 | runs-on: ubuntu-latest
13 | env:
14 | DOTNET_CLI_TELEMETRY_OPTOUT: "true"
15 | steps:
16 | - uses: actions/checkout@v3
17 | with:
18 | fetch-depth: 0
19 |
20 | - uses: actions/github-script@v6
21 | with:
22 | script: |
23 | const get_change_log_diff = require('./scripts/get_changelog_diff.js')
24 | core.exportVariable('CHANGELOG', get_change_log_diff())
25 |
26 | // Getting the release version from the PR source branch
27 | // Source branch looks like this: release-1.0.0
28 | const version = context.payload.pull_request.head.ref.split('-')[1]
29 | core.exportVariable('VERSION', version)
30 |
31 | - name: Setup dotnet
32 | uses: actions/setup-dotnet@v2
33 | with:
34 | dotnet-version: "6.0.x"
35 |
36 | - name: Create the package
37 | run: dotnet pack --configuration Release ./src
38 |
39 | - name: Publish the package
40 | run: dotnet nuget push "./src/bin/Release/*.nupkg" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate
41 |
42 | - name: Create release on GitHub
43 | uses: ncipollo/release-action@v1
44 | with:
45 | body: ${{ env.CHANGELOG }}
46 | tag: ${{ env.VERSION }}
47 | token: ${{ secrets.GITHUB_TOKEN }}
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015-2022 Stream.io Inc, and individual contributors.
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification, are permitted
6 | provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this list of
9 | conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of
12 | conditions and the following disclaimer in the documentation and/or other materials
13 | provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its contributors may
16 | be used to endorse or promote products derived from this software without specific prior
17 | written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
20 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
21 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
26 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/src/Models/Og.cs:
--------------------------------------------------------------------------------
1 | namespace Stream.Models
2 | {
3 | using System.Collections.Generic;
4 |
5 | public class OgImage
6 | {
7 | public string Image { get; set; }
8 | public string Url { get; set; }
9 | public string SecureUrl { get; set; }
10 | public int Width { get; set; }
11 | public int Height { get; set; }
12 | public string Type { get; set; }
13 | public string Alt { get; set; }
14 | }
15 |
16 | public class OgVideo
17 | {
18 | public string Video { get; set; }
19 | public string Url { get; set; }
20 | public string SecureUrl { get; set; }
21 | public int Width { get; set; }
22 | public int Height { get; set; }
23 | public string Type { get; set; }
24 | }
25 |
26 | public class OgAudio
27 | {
28 | public string Audio { get; set; }
29 | public string URL { get; set; }
30 | public string SecureUrl { get; set; }
31 | public string Type { get; set; }
32 | }
33 |
34 | public class Og : ResponseBase
35 | {
36 | public string Title { get; set; }
37 | public string Type { get; set; }
38 | public string Url { get; set; }
39 | public string Site { get; set; }
40 | public string SiteName { get; set; }
41 | public string Description { get; set; }
42 | public string Favicon { get; set; }
43 | public string Determiner { get; set; }
44 | public List Images { get; set; }
45 | public List Videos { get; set; }
46 | public List Audios { get; set; }
47 | }
48 | }
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/Models/GenericData.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Newtonsoft.Json.Linq;
3 | using Stream.Utils;
4 | using System;
5 |
6 | namespace Stream.Models
7 | {
8 | [JsonConverter(typeof(GenericDataConverter))]
9 | public class GenericData : CustomDataBase
10 | {
11 | /// Identifier of the object.
12 | public string Id { get; set; }
13 | }
14 |
15 | internal class GenericDataConverter : JsonConverter
16 | {
17 | public override bool CanConvert(Type objectType) => false;
18 | public override bool CanWrite => false;
19 |
20 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
21 | {
22 | var token = JToken.Load(reader);
23 | var result = new GenericData();
24 |
25 | if (token.Type == JTokenType.String)
26 | {
27 | result.Id = token.Value();
28 | return result;
29 | }
30 |
31 | if (token.Type == JTokenType.Object)
32 | {
33 | var obj = token as JObject;
34 | obj.Properties().ForEach(prop =>
35 | {
36 | if (prop.Name == "id")
37 | result.Id = prop.Value.Value();
38 | else
39 | result.SetData(prop.Name, prop.Value);
40 | });
41 |
42 | return result;
43 | }
44 |
45 | return result;
46 | }
47 |
48 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
49 | {
50 | throw new NotImplementedException();
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/tests/PersonalizationTests.cs:
--------------------------------------------------------------------------------
1 | using NUnit.Framework;
2 | using Stream.Models;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Threading.Tasks;
6 |
7 | namespace StreamNetTests
8 | {
9 | [Parallelizable(ParallelScope.None)]
10 | [TestFixture]
11 | public class PersonalizationTests : TestBase
12 | {
13 | [Test]
14 | [Ignore("Not always needed, set credentials to run when needed")]
15 | public async Task ReadPersonalization()
16 | {
17 | var response = await Client.Personalization.GetAsync("etoro_newsfeed", new Dictionary
18 | {
19 | { "feed_slug", "newsfeed" },
20 | { "user_id", "crembo" },
21 | { "limit", 20 },
22 | { "ranking", "etoro" },
23 | });
24 |
25 | var d = new Dictionary(response);
26 | Assert.AreEqual(41021, d["app_id"]);
27 | Assert.True(d.ContainsKey("duration"));
28 | Assert.True(d.ContainsKey("results"));
29 | }
30 |
31 | [Test]
32 | [Ignore("Not always needed, set credentials to run when needed")]
33 | public async Task ReadPersonalizedFeed()
34 | {
35 | var options = GetOptions.Default.
36 | WithEndpoint("etoro_newsfeed").
37 | WithFeedSlug("newsfeed").
38 | WithRanking("etoro").
39 | WithUserId(Guid.NewGuid().ToString());
40 |
41 | var response = await Client.GetPersonalizedFeedAsync(options);
42 | Assert.AreEqual(20, response.Limit);
43 | Assert.AreEqual(0, response.Offset);
44 | Assert.AreEqual(response.Results.Count, 0);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Models/UnfollowRelation.cs:
--------------------------------------------------------------------------------
1 | namespace Stream.Models
2 | {
3 | ///
4 | /// Represents a relationship to unfollow in batch operations
5 | ///
6 | public class UnfollowRelation
7 | {
8 | ///
9 | /// Source feed id
10 | ///
11 | public string Source { get; set; }
12 |
13 | ///
14 | /// Target feed id
15 | ///
16 | public string Target { get; set; }
17 |
18 | ///
19 | /// Whether to keep activities from the unfollowed feed
20 | ///
21 | public bool KeepHistory { get; set; }
22 |
23 | ///
24 | /// Creates a new instance of the UnfollowRelation class
25 | ///
26 | /// Source feed id
27 | /// Target feed id
28 | /// Whether to keep activities from the unfollowed feed
29 | public UnfollowRelation(string source, string target, bool keepHistory = false)
30 | {
31 | Source = source;
32 | Target = target;
33 | KeepHistory = keepHistory;
34 | }
35 |
36 | ///
37 | /// Creates a new instance of the UnfollowRelation class
38 | ///
39 | /// Source feed
40 | /// Target feed
41 | /// Whether to keep activities from the unfollowed feed
42 | public UnfollowRelation(IStreamFeed source, IStreamFeed target, bool keepHistory = false)
43 | {
44 | Source = source.FeedId;
45 | Target = target.FeedId;
46 | KeepHistory = keepHistory;
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/IUsers.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 |
4 | namespace Stream
5 | {
6 | ///
7 | /// Client to interact with users.
8 | /// Stream allows you to store user information and embed them inside
9 | /// activities or use them for personalization. When stored in activities,
10 | /// users are automatically enriched by Stream.
11 | ///
12 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
13 | public interface IUsers
14 | {
15 | /// Creates a new user.
16 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/users_introduction
17 | Task AddAsync(string userId, IDictionary data = null, bool getOrCreate = false);
18 |
19 | /// Deletes a user.
20 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/users_introduction
21 | Task DeleteAsync(string userId);
22 |
23 | /// Retrieves a user.
24 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/users_introduction
25 | Task GetAsync(string userId);
26 |
27 | /// Updates a user.
28 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/users_introduction
29 | Task UpdateAsync(string userId, IDictionary data);
30 |
31 | /// Returns a reference identifier to the user id.
32 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/users_introduction
33 | string Ref(string userId);
34 |
35 | /// Returns a reference identifier to the user object.
36 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/users_introduction
37 | string Ref(User obj);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/Utils/FeedIdValidator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Stream.Utils
4 | {
5 | public static class FeedIdValidator
6 | {
7 | ///
8 | /// Validates a fully qualified feed identifier. It should look like this: flat:myfeedname
9 | /// We could use a Regex but that has a performance impact
10 | /// so let's just iterate through the string and check for the correct format.
11 | ///
12 | public static void ThrowIfFeedIdIsInvalid(string feedId)
13 | {
14 | if (string.IsNullOrWhiteSpace(feedId))
15 | {
16 | throw new InvalidFeedIdException(feedId);
17 | }
18 |
19 | var foundColon = false;
20 | var colonIndex = 0;
21 | var index = 0;
22 |
23 | foreach (var character in feedId)
24 | {
25 | if (character == ':')
26 | {
27 | if (foundColon)
28 | {
29 | throw new InvalidFeedIdException(feedId);
30 | }
31 |
32 | if (index == 0 || index == feedId.Length - 1)
33 | {
34 | throw new InvalidFeedIdException(feedId);
35 | }
36 |
37 | foundColon = true;
38 | colonIndex = index;
39 | }
40 |
41 | index++;
42 | }
43 |
44 | if (!foundColon)
45 | {
46 | throw new InvalidFeedIdException(feedId);
47 | }
48 | }
49 | }
50 |
51 | ///
52 | /// Exception thrown when a feed identifier is invalid.
53 | /// The feed identifier should have a single colon. Example: flat:myfeedname
54 | ///
55 | public class InvalidFeedIdException : Exception
56 | {
57 | public InvalidFeedIdException(string feedId) : base($"Invalid feed id: {feedId}. It should look like this: flat:myfeedname")
58 | {
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/src/StreamClientOptions.cs:
--------------------------------------------------------------------------------
1 | namespace Stream
2 | {
3 | /// Customization options for the internal HTTP client.
4 | public class StreamClientOptions
5 | {
6 | /// Default settings where the backend location is .
7 | public static StreamClientOptions Default => new StreamClientOptions();
8 |
9 | ///
10 | /// Number of milliseconds to wait on requests
11 | ///
12 | /// Default is 3000
13 | public int Timeout { get; set; } = 3000;
14 |
15 | ///
16 | /// Number of milliseconds to wait on requests to personalization
17 | ///
18 | /// Default is 3000
19 | public int PersonalizationTimeout { get; set; } = 3000;
20 |
21 | ///
22 | /// Backend location of Stream API.
23 | ///
24 | /// Default is US East
25 | public StreamApiLocation Location { get; set; } = StreamApiLocation.USEast;
26 |
27 | ///
28 | /// Personalization backend location.
29 | ///
30 | /// Default is US East.
31 | public StreamApiLocation PersonalizationLocation { get; set; } = StreamApiLocation.USEast;
32 | }
33 |
34 | /// Physical location of the backend.
35 | public enum StreamApiLocation
36 | {
37 | /// United States, east coast
38 | USEast,
39 |
40 | /// Dublin
41 | Dublin,
42 |
43 | /// Tokyo
44 | Tokyo,
45 |
46 | /// Mumbai
47 | Mumbai,
48 |
49 | /// Singapore
50 | Singapore,
51 |
52 | /// Sidney
53 | Sidney,
54 |
55 | /// Oregon
56 | Oregon,
57 |
58 | /// Ohio
59 | Ohio,
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Utils/StreamJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Newtonsoft.Json.Serialization;
3 |
4 | namespace Stream.Utils
5 | {
6 | public static class StreamJsonConverter
7 | {
8 | private static JsonSerializerSettings Settings = new JsonSerializerSettings
9 | {
10 | DateFormatString = "yyyy-MM-dd'T'HH:mm:ss.fff",
11 | ContractResolver = new DefaultContractResolver
12 | {
13 | NamingStrategy = new SnakeCaseNamingStrategy(), // this handles ForeignId => foreign_id etc. conversion for us
14 | },
15 | NullValueHandling = NullValueHandling.Ignore,
16 | DateTimeZoneHandling = DateTimeZoneHandling.Utc, // always convert time to UTC
17 | };
18 |
19 | public static JsonSerializer Serializer { get; } = JsonSerializer.Create(Settings);
20 |
21 | public static string SerializeObject(object obj) =>
22 | JsonConvert.SerializeObject(obj, Settings);
23 |
24 | public static T DeserializeObject(string json) =>
25 | JsonConvert.DeserializeObject(json, Settings);
26 | }
27 |
28 | public static class StreamJsonConverterUTC
29 | {
30 | private static JsonSerializerSettings Settings = new JsonSerializerSettings
31 | {
32 | DateFormatString = "yyyy-MM-dd'T'HH:mm:ssZ",
33 | ContractResolver = new DefaultContractResolver
34 | {
35 | NamingStrategy = new SnakeCaseNamingStrategy(), // this handles ForeignId => foreign_id etc. conversion for us
36 | },
37 | NullValueHandling = NullValueHandling.Ignore,
38 | DateTimeZoneHandling = DateTimeZoneHandling.Utc, // always convert time to UTC
39 | };
40 |
41 | public static JsonSerializer Serializer { get; } = JsonSerializer.Create(Settings);
42 |
43 | public static string SerializeObject(object obj) =>
44 | JsonConvert.SerializeObject(obj, Settings);
45 |
46 | public static T DeserializeObject(string json) =>
47 | JsonConvert.DeserializeObject(json, Settings);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/ActivityTests/RemoveActivityTests.cs:
--------------------------------------------------------------------------------
1 | using NUnit.Framework;
2 | using Stream.Models;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace StreamNetTests
7 | {
8 | [TestFixture]
9 | public class RemoveActivityTests : TestBase
10 | {
11 | [Test]
12 | public async Task TestRemoveActivity()
13 | {
14 | var newActivity = new Activity("1", "test", "1");
15 | var response = await this.UserFeed.AddActivityAsync(newActivity);
16 | Assert.IsNotNull(response);
17 |
18 | var activities = (await this.UserFeed.GetActivitiesAsync(0, 1)).Results;
19 | Assert.IsNotNull(activities);
20 | Assert.AreEqual(1, activities.Count());
21 |
22 | var first = activities.FirstOrDefault();
23 | Assert.AreEqual(response.Id, first.Id);
24 |
25 | await this.UserFeed.RemoveActivityAsync(first.Id);
26 |
27 | var nextActivities = (await this.UserFeed.GetActivitiesAsync(0, 1)).Results;
28 | Assert.IsNotNull(nextActivities);
29 | Assert.IsFalse(nextActivities.Any(na => na.Id == first.Id));
30 | }
31 |
32 | [Test]
33 | public async Task TestRemoveActivityByForeignId()
34 | {
35 | var fid = "post:42";
36 | var newActivity = new Activity("1", "test", "1")
37 | {
38 | ForeignId = fid,
39 | };
40 |
41 | var response = await this.UserFeed.AddActivityAsync(newActivity);
42 | Assert.IsNotNull(response);
43 |
44 | var activities = (await this.UserFeed.GetActivitiesAsync(0, 1)).Results;
45 | Assert.IsNotNull(activities);
46 | Assert.AreEqual(1, activities.Count());
47 | Assert.AreEqual(response.Id, activities.First().Id);
48 | Assert.AreEqual(fid, activities.First().ForeignId);
49 |
50 | await this.UserFeed.RemoveActivityAsync(fid, true);
51 |
52 | activities = (await this.UserFeed.GetActivitiesAsync(0, 1)).Results;
53 | Assert.AreEqual(0, activities.Count());
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Models/ActivityMarker.cs:
--------------------------------------------------------------------------------
1 | using Stream.Rest;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace Stream.Models
6 | {
7 | public class ActivityMarker
8 | {
9 | private bool _allRead = false;
10 | private bool _allSeen = false;
11 | private List _read = new List();
12 | private List _seen = new List();
13 |
14 | private ActivityMarker()
15 | {
16 | }
17 |
18 | public ActivityMarker AllRead()
19 | {
20 | _allRead = true;
21 | return this;
22 | }
23 |
24 | public ActivityMarker AllSeen()
25 | {
26 | _allSeen = true;
27 | return this;
28 | }
29 |
30 | public ActivityMarker Read(params string[] activityIds)
31 | {
32 | if ((!_allRead) && (activityIds != null))
33 | _read = _read.Union(activityIds).Distinct().ToList();
34 | return this;
35 | }
36 |
37 | public ActivityMarker Seen(params string[] activityIds)
38 | {
39 | if ((!_allSeen) && (activityIds != null))
40 | _seen = _seen.Union(activityIds).Distinct().ToList();
41 | return this;
42 | }
43 |
44 | internal void Apply(RestRequest request)
45 | {
46 | // reads
47 | if (_allRead)
48 | {
49 | request.AddQueryParameter("mark_read", "true");
50 | }
51 | else if (_read.Count > 0)
52 | {
53 | request.AddQueryParameter("mark_read", string.Join(",", _read));
54 | }
55 |
56 | // seen
57 | if (_allSeen)
58 | {
59 | request.AddQueryParameter("mark_seen", "true");
60 | }
61 | else if (_seen.Count > 0)
62 | {
63 | request.AddQueryParameter("mark_seen", string.Join(",", _seen));
64 | }
65 | }
66 |
67 | public static ActivityMarker Mark()
68 | {
69 | return new ActivityMarker();
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Models/Activity.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 |
5 | namespace Stream.Models
6 | {
7 | public abstract class ActivityBase : CustomDataBase
8 | {
9 | public string Id { get; set; }
10 | public string Verb { get; set; }
11 | public string ForeignId { get; set; }
12 | public DateTime? Time { get; set; }
13 | public List To { get; set; }
14 | public float? Score { get; set; }
15 |
16 | public string Ref() => $"SA:{Id}";
17 | }
18 |
19 | public class Activity : ActivityBase
20 | {
21 | public string Actor { get; set; }
22 | public string Object { get; set; }
23 | public string Target { get; set; }
24 | public string Origin { get; set; }
25 | public Dictionary ScoreVars { get; set; }
26 |
27 | public string ModerationTemplate { get; set; }
28 |
29 | public Activity(string actor, string verb, string @object)
30 | {
31 | Actor = actor;
32 | Verb = verb;
33 | Object = @object;
34 | }
35 | }
36 |
37 | public class AddActivityResponse : ResponseBase
38 | {
39 | public Activity Activity { get; set; }
40 | }
41 |
42 | public class AddActivitiesResponse : ResponseBase
43 | {
44 | public List Activities { get; set; }
45 | }
46 |
47 | public class ModerationResponse
48 | {
49 | public string Status { get; set; }
50 | [JsonProperty("recommended_action")]
51 | public string RecommendedAction { get; set; }
52 | public APIError APIError { get; set; }
53 | }
54 |
55 | public class APIError
56 | {
57 | public string Code { get; set; }
58 | public string Message { get; set; }
59 | }
60 |
61 | public class ActivityPartialUpdateRequestObject
62 | {
63 | public string Id { get; set; }
64 | public string ForeignId { get; set; }
65 | public Dictionary Set { get; set; }
66 | public IEnumerable Unset { get; set; }
67 | public DateTime? Time { get; set; }
68 | }
69 | }
--------------------------------------------------------------------------------
/src/Moderation.cs:
--------------------------------------------------------------------------------
1 | using Stream.Models;
2 | using Stream.Rest;
3 | using Stream.Utils;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Net;
7 | using System.Net.Http;
8 | using System.Reflection;
9 | using System.Threading.Tasks;
10 |
11 | namespace Stream
12 | {
13 | public class Moderation : IModeration
14 | {
15 | private readonly StreamClient _client;
16 |
17 | public Moderation(StreamClient client)
18 | {
19 | _client = client;
20 | }
21 |
22 | public async Task FlagUserAsync(string flaggingUserID, string flaggedUserID, string reason, IDictionary options = null)
23 | {
24 | return await FlagAsync("stream:user", flaggedUserID, flaggingUserID, reason, options);
25 | }
26 |
27 | public async Task FlagActivityAsync(string entityId, string entityCreatorID, string reason, IDictionary options = null)
28 | {
29 | return await FlagAsync("stream:feeds:v2:activity", entityId, entityCreatorID, reason, options);
30 | }
31 |
32 | public async Task FlagReactionAsync(string entityId, string entityCreatorID, string reason, IDictionary options = null)
33 | {
34 | return await FlagAsync("stream:feeds:v2:reaction", entityId, entityCreatorID, reason, options);
35 | }
36 |
37 | public async Task FlagAsync(string entityType, string entityId, string entityCreatorID,
38 | string reason, IDictionary options = null)
39 | {
40 | var request = _client.BuildAppRequest("moderation/flag", HttpMethod.Post);
41 | request.SetJsonBody(StreamJsonConverter.SerializeObject(new
42 | {
43 | user_id = entityCreatorID, entity_type = entityType, entity_id = entityId, reason,
44 | }));
45 |
46 | var response = await _client.MakeRequestAsync(request);
47 |
48 | if (response.StatusCode == HttpStatusCode.Created)
49 | return StreamJsonConverter.DeserializeObject(response.Content);
50 |
51 | throw StreamException.FromResponse(response);
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/Models/FeedFilter.cs:
--------------------------------------------------------------------------------
1 | using Stream.Rest;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace Stream.Models
6 | {
7 | public class FeedFilter
8 | {
9 | #pragma warning disable SA1300
10 | internal enum OpType
11 | {
12 | id_gte,
13 | id_gt,
14 | id_lte,
15 | id_lt,
16 | with_activity_data,
17 | }
18 | #pragma warning restore SA1300
19 |
20 | internal class OpEntry
21 | {
22 | internal OpType Type { get; set; }
23 |
24 | internal string Value { get; set; }
25 |
26 | internal OpEntry(OpType type, string value)
27 | {
28 | Type = type;
29 | Value = value;
30 | }
31 | }
32 |
33 | private readonly List _ops = new List();
34 |
35 | public FeedFilter WithActivityData()
36 | {
37 | _ops.Add(new OpEntry(OpType.with_activity_data, "true"));
38 | return this;
39 | }
40 |
41 | public FeedFilter IdGreaterThan(string id)
42 | {
43 | _ops.Add(new OpEntry(OpType.id_gt, id));
44 | return this;
45 | }
46 |
47 | public FeedFilter IdGreaterThanEqual(string id)
48 | {
49 | _ops.Add(new OpEntry(OpType.id_gte, id));
50 | return this;
51 | }
52 |
53 | public FeedFilter IdLessThan(string id)
54 | {
55 | _ops.Add(new OpEntry(OpType.id_lt, id));
56 | return this;
57 | }
58 |
59 | public FeedFilter IdLessThanEqual(string id)
60 | {
61 | _ops.Add(new OpEntry(OpType.id_lte, id));
62 | return this;
63 | }
64 |
65 | internal void Apply(RestRequest request)
66 | {
67 | _ops.ForEach(op =>
68 | {
69 | request.AddQueryParameter(op.Type.ToString(), op.Value);
70 | });
71 | }
72 |
73 | internal bool IncludesActivityData => _ops.Any(x => x.Type == OpType.with_activity_data);
74 |
75 | public static FeedFilter Where() => new FeedFilter();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tests/FeedTests.cs:
--------------------------------------------------------------------------------
1 | using NUnit.Framework;
2 | using Stream;
3 | using System;
4 |
5 | namespace StreamNetTests
6 | {
7 | [TestFixture]
8 | public class FeedTests : TestBase
9 | {
10 | private IStreamFeed _feed;
11 |
12 | [SetUp]
13 | public void SetupFeed()
14 | {
15 | _feed = Client.Feed("flat", "42");
16 | }
17 |
18 | [Test]
19 | public void TestFollowFeedArguments()
20 | {
21 | Assert.ThrowsAsync(async () =>
22 | {
23 | await _feed.FollowFeedAsync(null);
24 | });
25 | Assert.ThrowsAsync(async () =>
26 | {
27 | await _feed.FollowFeedAsync(_feed);
28 | });
29 | Assert.ThrowsAsync(async () =>
30 | {
31 | var feed = Client.Feed("flat", Guid.NewGuid().ToString());
32 | await _feed.FollowFeedAsync(feed, -1);
33 | });
34 | Assert.ThrowsAsync(async () =>
35 | {
36 | var feed = Client.Feed("flat", Guid.NewGuid().ToString());
37 | await _feed.FollowFeedAsync(feed, 1001);
38 | });
39 | Assert.DoesNotThrowAsync(async () =>
40 | {
41 | var feed = Client.Feed("flat", Guid.NewGuid().ToString());
42 | await _feed.FollowFeedAsync(feed, 0);
43 | });
44 | Assert.DoesNotThrowAsync(async () =>
45 | {
46 | var feed = Client.Feed("flat", Guid.NewGuid().ToString());
47 | await _feed.FollowFeedAsync(feed, 1000);
48 | });
49 | }
50 |
51 | [Test]
52 | public void TestUnfollowFeedArguments()
53 | {
54 | Assert.ThrowsAsync(async () =>
55 | {
56 | await _feed.UnfollowFeedAsync(null);
57 | });
58 | Assert.ThrowsAsync(async () =>
59 | {
60 | await _feed.UnfollowFeedAsync(_feed);
61 | });
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Models/CollectionObject.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Newtonsoft.Json.Linq;
3 | using Stream.Utils;
4 | using System;
5 | using System.Collections.Generic;
6 |
7 | namespace Stream.Models
8 | {
9 | public class CollectionObject
10 | {
11 | public CollectionObject(string id) => Id = id;
12 |
13 | public string Id { get; set; }
14 | public string UserId { get; set; }
15 | public GenericData Data { get; set; } = new GenericData();
16 |
17 | /// Returns a reference identifier to this object.
18 | public string Ref(string collectionName) => $"SO:{collectionName}:{Id}";
19 |
20 | /// Sets a custom data value.
21 | public void SetData(string name, T data) => Data.SetData(name, data, null);
22 |
23 | /// Sets multiple custom data.
24 | public void SetData(IEnumerable> data) => data.ForEach(x => SetData(x.Key, x.Value, null));
25 |
26 | ///
27 | /// Sets a custom data value. If is not null, it will be used to serialize the value.
28 | ///
29 | public void SetData(string name, T data, JsonSerializer serializer) => Data.SetData(name, data, serializer);
30 |
31 | ///
32 | /// Gets a custom data value parsed into .
33 | ///
34 | public T GetData(string name) => Data.GetData(name);
35 |
36 | internal JObject Flatten()
37 | {
38 | var flat = new JObject();
39 |
40 | if (!string.IsNullOrWhiteSpace(Id))
41 | flat["id"] = Id;
42 |
43 | if (!string.IsNullOrEmpty(UserId))
44 | flat["user_id"] = UserId;
45 |
46 | if (Data?.GetAllData()?.Count > 0)
47 | {
48 | foreach (var kvp in Data.GetAllData())
49 | flat[kvp.Key] = kvp.Value;
50 | }
51 |
52 | return flat;
53 | }
54 | }
55 |
56 | public class GetCollectionResponse
57 | {
58 | public List Data { get; set; }
59 | }
60 |
61 | public class GetCollectionResponseWrap : ResponseBase
62 | {
63 | public GetCollectionResponse Response { get; set; }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/stream-net.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27703.2042
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "stream-net", "src\stream-net.csproj", "{2EC51FFB-5F72-4691-BE8C-DD0FA5B77F4E}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "stream-net-tests", "tests\stream-net-tests.csproj", "{A2B7DA78-71AD-4A9F-B292-F40C54F98648}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{7CA14810-0835-4E42-BEE9-8C1D35E4E839}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "samples\ConsoleApp\ConsoleApp.csproj", "{97CCDEC8-5F41-445C-B645-8E972A8707E4}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {2EC51FFB-5F72-4691-BE8C-DD0FA5B77F4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {2EC51FFB-5F72-4691-BE8C-DD0FA5B77F4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {2EC51FFB-5F72-4691-BE8C-DD0FA5B77F4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {2EC51FFB-5F72-4691-BE8C-DD0FA5B77F4E}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {A2B7DA78-71AD-4A9F-B292-F40C54F98648}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {A2B7DA78-71AD-4A9F-B292-F40C54F98648}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {A2B7DA78-71AD-4A9F-B292-F40C54F98648}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {A2B7DA78-71AD-4A9F-B292-F40C54F98648}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {97CCDEC8-5F41-445C-B645-8E972A8707E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {97CCDEC8-5F41-445C-B645-8E972A8707E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {97CCDEC8-5F41-445C-B645-8E972A8707E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {97CCDEC8-5F41-445C-B645-8E972A8707E4}.Release|Any CPU.Build.0 = Release|Any CPU
32 | EndGlobalSection
33 | GlobalSection(SolutionProperties) = preSolution
34 | HideSolutionNode = FALSE
35 | EndGlobalSection
36 | GlobalSection(ExtensibilityGlobals) = postSolution
37 | SolutionGuid = {7C0E0352-183F-4D4D-B8AC-5132BC9C46AD}
38 | EndGlobalSection
39 | GlobalSection(NestedProjects) = preSolution
40 | {97CCDEC8-5F41-445C-B645-8E972A8707E4} = {7CA14810-0835-4E42-BEE9-8C1D35E4E839}
41 | EndGlobalSection
42 | EndGlobal
43 |
--------------------------------------------------------------------------------
/tests/UserTests.cs:
--------------------------------------------------------------------------------
1 | using NUnit.Framework;
2 | using Stream;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Threading.Tasks;
6 |
7 | namespace StreamNetTests
8 | {
9 | [TestFixture]
10 | public class UserTests : TestBase
11 | {
12 | [Test]
13 | public async Task TestUsers()
14 | {
15 | // Create user
16 | var userId = Guid.NewGuid().ToString();
17 | var userData = new Dictionary
18 | {
19 | { "field", "value" },
20 | { "is_admin", true },
21 | };
22 |
23 | var u = await Client.Users.AddAsync(userId, userData);
24 |
25 | Assert.NotNull(u);
26 | Assert.NotNull(u.CreatedAt);
27 | Assert.NotNull(u.UpdatedAt);
28 | Assert.AreEqual(userId, u.Id);
29 | Assert.AreEqual(userData, u.Data);
30 |
31 | Assert.ThrowsAsync(async () =>
32 | {
33 | u = await Client.Users.AddAsync(userId, userData);
34 | });
35 |
36 | var newUserData = new Dictionary()
37 | {
38 | { "field", "othervalue" },
39 | };
40 | Assert.DoesNotThrowAsync(async () =>
41 | {
42 | u = await Client.Users.AddAsync(userId, newUserData, true);
43 | });
44 | Assert.NotNull(u);
45 | Assert.NotNull(u.CreatedAt);
46 | Assert.NotNull(u.UpdatedAt);
47 | Assert.AreEqual(userId, u.Id);
48 | Assert.AreEqual(userData, u.Data);
49 |
50 | // Get user
51 | u = await Client.Users.GetAsync(userId);
52 | Assert.NotNull(u);
53 | Assert.NotNull(u.CreatedAt);
54 | Assert.NotNull(u.UpdatedAt);
55 | Assert.AreEqual(userId, u.Id);
56 | Assert.AreEqual(userData, u.Data);
57 |
58 | // Update user
59 | u = await Client.Users.UpdateAsync(userId, newUserData);
60 | Assert.NotNull(u);
61 | Assert.NotNull(u.CreatedAt);
62 | Assert.NotNull(u.UpdatedAt);
63 | Assert.AreEqual(userId, u.Id);
64 | Assert.AreEqual(newUserData, u.Data);
65 |
66 | // Delete user
67 | await Client.Users.DeleteAsync(userId);
68 |
69 | Assert.ThrowsAsync(async () =>
70 | {
71 | var x = await Client.Users.GetAsync(userId);
72 | });
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/src/Personalization.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Stream.Models;
3 | using Stream.Utils;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Net;
7 | using System.Net.Http;
8 | using System.Threading.Tasks;
9 |
10 | namespace Stream
11 | {
12 | public class Personalization : IPersonalization
13 | {
14 | private readonly StreamClient _client;
15 |
16 | internal Personalization(StreamClient client)
17 | {
18 | _client = client;
19 | }
20 |
21 | public async Task> GetAsync(string endpoint, IDictionary data)
22 | {
23 | var request = _client.BuildPersonalizationRequest(endpoint + "/", HttpMethod.Get);
24 | foreach (KeyValuePair entry in data)
25 | {
26 | request.AddQueryParameter(entry.Key, Convert.ToString(entry.Value));
27 | }
28 |
29 | var response = await _client.MakeRequestAsync(request);
30 | if (response.StatusCode == HttpStatusCode.OK)
31 | return StreamJsonConverter.DeserializeObject>(response.Content);
32 |
33 | throw StreamException.FromResponse(response);
34 | }
35 |
36 | public async Task> PostAsync(string endpoint, IDictionary data)
37 | {
38 | var request = _client.BuildPersonalizationRequest(endpoint + "/", HttpMethod.Post);
39 | request.SetJsonBody(StreamJsonConverter.SerializeObject(data));
40 |
41 | var response = await _client.MakeRequestAsync(request);
42 | if (response.StatusCode == HttpStatusCode.OK)
43 | return StreamJsonConverter.DeserializeObject>(response.Content);
44 |
45 | throw StreamException.FromResponse(response);
46 | }
47 |
48 | public async Task DeleteAsync(string endpoint, IDictionary data)
49 | {
50 | var request = _client.BuildPersonalizationRequest(endpoint + "/", HttpMethod.Delete);
51 | foreach (KeyValuePair entry in data)
52 | {
53 | request.AddQueryParameter(entry.Key, Convert.ToString(entry.Value));
54 | }
55 |
56 | var response = await _client.MakeRequestAsync(request);
57 |
58 | if ((int)response.StatusCode >= 300)
59 | throw StreamException.FromResponse(response);
60 |
61 | return StreamJsonConverter.DeserializeObject(response.Content);
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/stream-net.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | TRACE;DEBUG;NETCORE
4 |
5 |
6 | netstandard1.6;netstandard2.0;netstandard2.1;net6.0
7 |
8 |
9 | stream-net
10 | .NET Client for Stream Feeds
11 | 7.4.0
12 | Stream.io
13 | GetStream.io
14 | © $([System.DateTime]::UtcNow.ToString(yyyy)) Stream.io
15 | Client for getstream.io. Build scalable newsfeeds and activity streams in a few hours instead of weeks.
16 | https://github.com/GetStream/stream-net
17 | $(GITHUB_SHA)
18 | https://github.com/GetStream/stream-net
19 | LICENSE
20 | $(CHANGELOG)
21 | getstream.io stream.io feeds sdk
22 | README.md
23 | nuget_logo.png
24 | true
25 | true
26 | true
27 | true
28 | snupkg
29 | true
30 |
31 |
32 | true
33 |
34 |
35 | OLD_TLS_HANDLING
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | ../.stylecop.ruleset
52 |
53 |
54 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # :recycle: Contributing
2 |
3 | We welcome code changes that improve this library or fix a problem, please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github. We are very happy to merge your code in the official repository. Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first. See our license file for more details.
4 |
5 | ## Getting started
6 |
7 | ### Restore dependencies and build
8 |
9 | Most IDEs automatically prompt you to restore the dependencies but if not, use this command:
10 |
11 | ```shell
12 | $ dotnet restore ./src
13 | ```
14 |
15 | Building:
16 |
17 | ```shell
18 | $ dotnet build ./src
19 | ```
20 |
21 | ### Run tests
22 |
23 | The tests we have are full fledged integration tests, meaning they will actually reach out to a Stream app. Hence the tests require at least two environment variables: `STREAM_API_KEY` and `STREAM_API_SECRET`.
24 |
25 | To have these env vars available for the test running, you need to set up a `.runsettings` file in the root of the project (don't worry, it's gitignored).
26 |
27 | > :bulb: Microsoft has a super detailed [documentation](https://docs.microsoft.com/en-us/visualstudio/test/configure-unit-tests-by-using-a-dot-runsettings-file) about .runsettings file.
28 |
29 | It needs to look like this:
30 |
31 | ```xml
32 |
33 |
34 |
35 |
36 | api_key_here
37 | secret_key_here
38 |
39 |
40 |
41 | ```
42 |
43 | In CLI:
44 | ```shell
45 | $ dotnet test -s .runsettings
46 | ```
47 |
48 | Go to the next section to see how to use it in IDEs.
49 |
50 | ## Recommended tools for day-to-day development
51 |
52 | ### VS Code
53 |
54 | For VS Code, the recommended extensions are:
55 | - [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) by Microsoft
56 | - [.NET Core Test Explorer](https://marketplace.visualstudio.com/items?itemName=formulahendry.dotnet-test-explorer) by Jun Huan
57 |
58 | Recommended settings (`.vscode/settings.json`):
59 | ```json
60 | {
61 | "omnisharp.testRunSettings": ".runsettings",
62 | "dotnet-test-explorer.testProjectPath": "./tests",
63 | }
64 | ```
65 |
66 | ### Visual Studio
67 |
68 | Follow [Microsoft's documentation](https://docs.microsoft.com/en-us/visualstudio/test/configure-unit-tests-by-using-a-dot-runsettings-file?view=vs-2022#specify-a-run-settings-file-in-the-ide) on how to set up `.runsettings` file.
69 |
70 | ### Rider
71 |
72 | Follow [Jetbrain's documentation](https://www.jetbrains.com/help/rider/Reference__Options__Tools__Unit_Testing__MSTest.html) on how to set up `.runsettings` file.
73 |
--------------------------------------------------------------------------------
/src/UsersBatch.cs:
--------------------------------------------------------------------------------
1 | using Stream.Models;
2 | using Stream.Utils;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Net.Http;
7 | using System.Threading.Tasks;
8 |
9 | namespace Stream
10 | {
11 | public class UsersBatch : IUsersBatch
12 | {
13 | private readonly StreamClient _client;
14 |
15 | internal UsersBatch(StreamClient client)
16 | {
17 | _client = client;
18 | }
19 |
20 | public async Task> UpsertUsersAsync(IEnumerable users, bool overrideExisting = false)
21 | {
22 | var body = new Dictionary
23 | {
24 | { "users", users },
25 | { "override_existing", overrideExisting },
26 | };
27 | var request = _client.BuildAppRequest("users/", HttpMethod.Post);
28 | request.SetJsonBody(StreamJsonConverter.SerializeObject(body));
29 |
30 | var response = await _client.MakeRequestAsync(request);
31 |
32 | if (response.StatusCode == HttpStatusCode.Created)
33 | {
34 | var addUserBatchResponse = StreamJsonConverter.DeserializeObject(response.Content);
35 | return addUserBatchResponse.CreatedUsers;
36 | }
37 |
38 | throw StreamException.FromResponse(response);
39 | }
40 |
41 | public async Task> GetUsersAsync(IEnumerable userIds)
42 | {
43 | var request = _client.BuildAppRequest("users/", HttpMethod.Get);
44 | request.AddQueryParameter("ids", string.Join(",", userIds));
45 |
46 | var response = await _client.MakeRequestAsync(request);
47 |
48 | if (response.StatusCode == HttpStatusCode.OK)
49 | {
50 | var getUserBatchResponse = StreamJsonConverter.DeserializeObject(response.Content);
51 | return getUserBatchResponse.Users;
52 | }
53 |
54 | throw StreamException.FromResponse(response);
55 | }
56 |
57 | public async Task> DeleteUsersAsync(IEnumerable userIds)
58 | {
59 | var request = _client.BuildAppRequest("users/", HttpMethod.Delete);
60 | request.AddQueryParameter("ids", string.Join(",", userIds));
61 |
62 | var response = await _client.MakeRequestAsync(request);
63 |
64 | if (response.StatusCode == HttpStatusCode.OK)
65 | {
66 | var deleteUserBatchResponse = StreamJsonConverter.DeserializeObject(response.Content);
67 | return deleteUserBatchResponse.DeletedUserIds;
68 | }
69 |
70 | throw StreamException.FromResponse(response);
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/src/Users.cs:
--------------------------------------------------------------------------------
1 | using Stream.Utils;
2 | using System.Collections.Generic;
3 | using System.Net;
4 | using System.Net.Http;
5 | using System.Threading.Tasks;
6 |
7 | namespace Stream
8 | {
9 | public class Users : IUsers
10 | {
11 | private readonly StreamClient _client;
12 |
13 | internal Users(StreamClient client)
14 | {
15 | _client = client;
16 | }
17 |
18 | public async Task AddAsync(string userId, IDictionary data = null, bool getOrCreate = false)
19 | {
20 | var u = new User
21 | {
22 | Id = userId,
23 | Data = data,
24 | };
25 | var request = _client.BuildAppRequest("user/", HttpMethod.Post);
26 |
27 | request.SetJsonBody(StreamJsonConverter.SerializeObject(u));
28 | request.AddQueryParameter("get_or_create", getOrCreate.ToString());
29 |
30 | var response = await _client.MakeRequestAsync(request);
31 |
32 | if (response.StatusCode == HttpStatusCode.Created)
33 | return StreamJsonConverter.DeserializeObject(response.Content);
34 |
35 | throw StreamException.FromResponse(response);
36 | }
37 |
38 | public async Task GetAsync(string userId)
39 | {
40 | var request = _client.BuildAppRequest($"user/{userId}/", HttpMethod.Get);
41 |
42 | var response = await _client.MakeRequestAsync(request);
43 |
44 | if (response.StatusCode == HttpStatusCode.OK)
45 | return StreamJsonConverter.DeserializeObject(response.Content);
46 |
47 | throw StreamException.FromResponse(response);
48 | }
49 |
50 | public async Task UpdateAsync(string userId, IDictionary data)
51 | {
52 | var u = new User
53 | {
54 | Data = data,
55 | };
56 | var request = _client.BuildAppRequest($"user/{userId}/", HttpMethod.Put);
57 | request.SetJsonBody(StreamJsonConverter.SerializeObject(u));
58 |
59 | var response = await _client.MakeRequestAsync(request);
60 |
61 | if (response.StatusCode == HttpStatusCode.Created)
62 | return StreamJsonConverter.DeserializeObject(response.Content);
63 |
64 | throw StreamException.FromResponse(response);
65 | }
66 |
67 | public async Task DeleteAsync(string userId)
68 | {
69 | var request = _client.BuildAppRequest($"user/{userId}/", HttpMethod.Delete);
70 |
71 | var response = await _client.MakeRequestAsync(request);
72 |
73 | if (response.StatusCode != HttpStatusCode.OK)
74 | throw StreamException.FromResponse(response);
75 | }
76 |
77 | public string Ref(string userId) => Ref(new User { Id = userId });
78 | public string Ref(User obj) => obj.Ref();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/StreamException.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Stream.Models;
3 | using Stream.Rest;
4 | using Stream.Utils;
5 | using System;
6 | using System.Net;
7 |
8 | namespace Stream
9 | {
10 | internal class ExceptionResponse : ResponseBase
11 | {
12 | public int Code { get; set; }
13 | public string Detail { get; set; }
14 | public string Exception { get; set; }
15 | public string MoreInfo { get; set; }
16 |
17 | [JsonIgnore]
18 | public HttpStatusCode HttpStatusCode { get; set; }
19 | }
20 |
21 | ///
22 | /// Generic exception that was thrown by the backend.
23 | /// For more details check the property. The error code
24 | /// that can be found in the property can be check in
25 | /// the below webpage.
26 | ///
27 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/api_error_responses/?language=csharp
28 | #if !NETCORE
29 | [Serializable]
30 | #endif
31 | public class StreamException : Exception
32 | {
33 | internal StreamException(ExceptionResponse state) : base($"{state.Exception}: {state.Detail}l")
34 | {
35 | ErrorCode = state.Code;
36 | HttpStatusCode = state.HttpStatusCode;
37 | Detail = state.Detail;
38 | MoreInfo = state.MoreInfo;
39 | }
40 |
41 | /// Error code from the backend.
42 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/api_error_responses/?language=csharp
43 | public int ErrorCode { get; }
44 |
45 | /// HTTP code returned from the backend.
46 | public HttpStatusCode HttpStatusCode { get; }
47 |
48 | /// Details of the error.
49 | public string Detail { get; }
50 |
51 | /// An URL that provides more information about the error.
52 | public string MoreInfo { get; }
53 |
54 | internal static StreamException FromResponse(RestResponse response)
55 | {
56 | // If we get an error response from getstream.io with the following structure then use it to populate the exception details,
57 | // otherwise fill in the properties from the response, the most likely case being when we get a timeout.
58 | // {"code": 6, "detail": "The following feeds are not configured: 'secret'", "duration": "4ms", "exception": "FeedConfigException", "status_code": 400}
59 | ExceptionResponse state = null;
60 |
61 | if (!string.IsNullOrWhiteSpace(response.Content) && response.Content.TrimStart().StartsWith("{"))
62 | {
63 | state = StreamJsonConverter.DeserializeObject(response.Content);
64 | }
65 |
66 | state = state ?? new ExceptionResponse();
67 | state.HttpStatusCode = response.StatusCode;
68 |
69 | throw new StreamException(state);
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/UtilsTests.cs:
--------------------------------------------------------------------------------
1 | using NUnit.Framework;
2 | using Stream.Models;
3 | using Stream.Utils;
4 | using System;
5 | using System.Threading.Tasks;
6 |
7 | namespace StreamNetTests
8 | {
9 | [TestFixture]
10 | public class UtilsTests : TestBase
11 | {
12 | [Test]
13 | public void TestIdGenerator()
14 | {
15 | // All these test cases are copied from the backend Go implementation
16 | var first = ActivityIdGenerator.GenerateId("123sdsd333}}}", 1451260800);
17 | Assert.AreEqual("f01c0000-acf5-11e5-8080-80006a8b5bc2", first.ToString());
18 |
19 | var second = ActivityIdGenerator.GenerateId("6621934", 1452862482);
20 | Assert.AreEqual("24f9d500-bb87-11e5-8080-80012ce3cc51", second.ToString());
21 |
22 | var third = ActivityIdGenerator.GenerateId("6621938", 1452862489);
23 | Assert.AreEqual("2925f280-bb87-11e5-8080-8001597d791f", third.ToString());
24 |
25 | var fourth = ActivityIdGenerator.GenerateId("6621941", 1452862492);
26 | Assert.AreEqual("2aefb600-bb87-11e5-8080-8001226fe2f2", fourth.ToString());
27 |
28 | var fifth = ActivityIdGenerator.GenerateId("6621945", 1452862496);
29 | Assert.AreEqual("2d521000-bb87-11e5-8080-800026259780", fifth.ToString());
30 |
31 | var sixth = ActivityIdGenerator.GenerateId("top_issues_summary_557dc1d9e46fea0a4c000002", 1463914800);
32 | Assert.AreEqual("53dbf800-200c-11e6-8080-800023ec2877", sixth.ToString());
33 |
34 | var seventh = ActivityIdGenerator.GenerateId("6625387", 1452866055);
35 | Assert.AreEqual("76a65d80-bb8f-11e5-8080-8000530672db", seventh.ToString());
36 |
37 | var eight = ActivityIdGenerator.GenerateId("UserPrediction:11127278", 1480174117);
38 | Assert.AreEqual("00000080-b3ed-11e6-8080-8000444ef374", eight.ToString());
39 |
40 | var nineth = ActivityIdGenerator.GenerateId("467791-42-follow", 1481475921);
41 | Assert.AreEqual("ffa65e80-bfc3-11e6-8080-800027086507", nineth.ToString());
42 | }
43 |
44 | [Test]
45 | public async Task TestActivityIdSameAsBackend()
46 | {
47 | var time = DateTime.UtcNow;
48 | var foreignId = Guid.NewGuid().ToString();
49 | var inputAct = new Activity("1", "test", "1") { Time = time, ForeignId = foreignId };
50 | var activity = await this.UserFeed.AddActivityAsync(inputAct);
51 |
52 | Assert.AreEqual(ActivityIdGenerator.GenerateId(activity.ForeignId, time).ToString(), activity.Id);
53 | }
54 |
55 | [Test]
56 | public void TestStreamJsonConverterUTC()
57 | {
58 | var date0 = new DateTime(2023, 5, 10, 12, 30, 15, DateTimeKind.Utc);
59 | var date0AsJsonNewtonsoft = Newtonsoft.Json.JsonConvert.SerializeObject(date0);
60 | var date0AsJson = Stream.Utils.StreamJsonConverterUTC.SerializeObject(date0);
61 |
62 | Assert.AreEqual(date0AsJsonNewtonsoft, date0AsJson);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/UsersBatchTests.cs:
--------------------------------------------------------------------------------
1 | using NUnit.Framework;
2 | using Stream;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 |
8 | namespace StreamNetTests
9 | {
10 | [TestFixture]
11 | public class UsersBatchTests : TestBase
12 | {
13 | [Test]
14 | public async Task TestAddGetUsersAsync()
15 | {
16 | var userIds = new List { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() };
17 |
18 | var users = new List
19 | {
20 | new User { Id = userIds[0], Data = new Dictionary { { "field", "value1" } } },
21 | new User { Id = userIds[1], Data = new Dictionary { { "field", "value2" } } },
22 | };
23 |
24 | var response = await Client.UsersBatch.UpsertUsersAsync(users);
25 |
26 | Assert.NotNull(response);
27 | Assert.AreEqual(users.Count, response.Count());
28 |
29 | var usersReturned = await Client.UsersBatch.GetUsersAsync(userIds);
30 |
31 | Assert.NotNull(usersReturned);
32 | Assert.AreEqual(userIds.Count, usersReturned.Count());
33 | }
34 |
35 | [Test]
36 | public async Task TestDeleteUsersAsync()
37 | {
38 | var userIds = new List { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() };
39 |
40 | var deletedUserIds = await Client.UsersBatch.DeleteUsersAsync(userIds);
41 |
42 | Assert.NotNull(deletedUserIds);
43 | Assert.AreEqual(userIds.Count, deletedUserIds.Count());
44 | Assert.IsTrue(userIds.All(id => deletedUserIds.Contains(id)));
45 | }
46 |
47 | [Test]
48 | public async Task AddGetDeleteGetUsersAsync()
49 | {
50 | var userIds = new List { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() };
51 |
52 | // Add users
53 | var users = new List
54 | {
55 | new User { Id = userIds[0], Data = new Dictionary { { "field", "value1" } } },
56 | new User { Id = userIds[1], Data = new Dictionary { { "field", "value2" } } },
57 | };
58 |
59 | var addResponse = await Client.UsersBatch.UpsertUsersAsync(users);
60 | Assert.NotNull(addResponse);
61 | Assert.AreEqual(users.Count, addResponse.Count());
62 |
63 | // Get users to confirm they were added
64 | var getUsersResponse = await Client.UsersBatch.GetUsersAsync(userIds);
65 | Assert.NotNull(getUsersResponse);
66 | Assert.AreEqual(users.Count, getUsersResponse.Count());
67 |
68 | // Delete users
69 | var deleteResponse = await Client.UsersBatch.DeleteUsersAsync(userIds);
70 | Assert.NotNull(deleteResponse);
71 | Assert.AreEqual(userIds.Count(), deleteResponse.Count());
72 | Assert.IsTrue(userIds.All(id => deleteResponse.Contains(id)));
73 |
74 | // Attempt to get deleted users to confirm they were deleted
75 | var getDeletedUsersResponse = await Client.UsersBatch.GetUsersAsync(userIds);
76 | Assert.IsEmpty(getDeletedUsersResponse);
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/src/Models/CustomDataBase.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Newtonsoft.Json.Linq;
3 | using Stream.Utils;
4 | using System.Collections.Generic;
5 |
6 | namespace Stream.Models
7 | {
8 | public abstract class CustomDataBase
9 | {
10 | [JsonExtensionData]
11 | protected virtual Dictionary Data { get; set; } = new Dictionary();
12 |
13 | ///
14 | /// Returns all custom data
15 | ///
16 | public Dictionary GetAllData() => Data;
17 |
18 | ///
19 | /// Gets a custom data value parsed into
20 | ///
21 | public T GetData(string name) => GetData(name, null);
22 |
23 | ///
24 | /// Gets a custom data value parsed into
25 | /// If is not null, it will be used to de-serialize the value.
26 | ///
27 | public T GetData(string name, JsonSerializer serializer) => GetDataInternal(name, serializer);
28 |
29 | ///
30 | /// Gets a custom data value parsed into .
31 | /// If it doesn't exist, it returns .
32 | ///
33 | public T GetDataOrDefault(string name, T @default) => Data.TryGetValue(name, out var val) ? val.ToObject() : @default;
34 |
35 | private T GetDataInternal(string name, JsonSerializer serializer)
36 | {
37 | if (Data.TryGetValue(name, out var val))
38 | {
39 | // Hack logic:
40 | // Sometimes our customers provide raw json strings instead of objects.
41 | // So for example:
42 | // SetData("stringcomplex", "{ \"test1\": 1, \"test2\": \"testing\" }");
43 | // instead of
44 | // SetData("stringcomplex", new Dictionary { { "test1", 1 }, { "test2", "testing" } });
45 | if (val.Type == JTokenType.String && val.Value().StartsWith("{") && val.Value().EndsWith("}"))
46 | {
47 | return StreamJsonConverter.DeserializeObject(val.Value());
48 | }
49 |
50 | if (serializer != null)
51 | {
52 | return val.ToObject(serializer);
53 | }
54 |
55 | return val.ToObject();
56 | }
57 |
58 | return default(T);
59 | }
60 |
61 | /// Sets a custom data value.
62 | public void SetData(string name, T data) => SetData(name, data, null);
63 |
64 | /// Sets multiple custom data.
65 | public void SetData(IEnumerable> data) => data.ForEach(x => SetData(x.Key, x.Value, null));
66 |
67 | ///
68 | /// Sets a custom data value. If is not null, it will be used to serialize the value.
69 | ///
70 | public void SetData(string name, T data, JsonSerializer serializer)
71 | {
72 | if (serializer != null)
73 | Data[name] = JValue.FromObject(data, serializer);
74 | else
75 | Data[name] = JValue.FromObject(data);
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/src/StreamClientToken.cs:
--------------------------------------------------------------------------------
1 | using Stream.Utils;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Security.Cryptography;
5 | using System.Text;
6 |
7 | namespace Stream
8 | {
9 | public interface IToken
10 | {
11 | string CreateUserToken(string userId, IDictionary extraData = null);
12 |
13 | string For(object payload);
14 | }
15 |
16 | public static class TokenFactory
17 | {
18 | public static IToken For(string apiSecretOrToken)
19 | {
20 | return apiSecretOrToken.Contains(".")
21 | ? (IToken)new Token(apiSecretOrToken)
22 | : (IToken)new Secret(apiSecretOrToken);
23 | }
24 | }
25 |
26 | public class Token : IToken
27 | {
28 | private readonly string _sessionToken;
29 |
30 | public Token(string sessionToken)
31 | {
32 | _sessionToken = sessionToken;
33 | }
34 |
35 | public string CreateUserToken(string userId, IDictionary extraData = null)
36 | {
37 | throw new InvalidOperationException("Clients connecting using a user token cannot create additional user tokens");
38 | }
39 |
40 | public string For(object payload) => _sessionToken;
41 | }
42 |
43 | public class Secret : IToken
44 | {
45 | private readonly string _apiSecret;
46 | private static readonly object JWTHeader = new
47 | {
48 | typ = "JWT",
49 | alg = "HS256",
50 | };
51 |
52 | public Secret(string apiSecret)
53 | {
54 | _apiSecret = apiSecret;
55 | }
56 |
57 | private static string Base64UrlEncode(byte[] input)
58 | {
59 | return Convert.ToBase64String(input)
60 | .Replace('+', '-')
61 | .Replace('/', '_')
62 | .Trim('=');
63 | }
64 |
65 | public string CreateUserToken(string userId, IDictionary extraData = null)
66 | {
67 | var payload = new Dictionary
68 | {
69 | { "user_id", userId },
70 | };
71 |
72 | if (extraData != null)
73 | {
74 | extraData.ForEach(x => payload[x.Key] = x.Value);
75 | }
76 |
77 | return For(payload);
78 | }
79 |
80 | public string For(object payload)
81 | {
82 | var segments = new List();
83 |
84 | byte[] headerBytes = Encoding.UTF8.GetBytes(StreamJsonConverter.SerializeObject(JWTHeader));
85 | byte[] payloadBytes = Encoding.UTF8.GetBytes(StreamJsonConverter.SerializeObject(payload));
86 |
87 | segments.Add(Base64UrlEncode(headerBytes));
88 | segments.Add(Base64UrlEncode(payloadBytes));
89 |
90 | var stringToSign = string.Join(".", segments);
91 | var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);
92 |
93 | using (var sha = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret)))
94 | {
95 | byte[] signature = sha.ComputeHash(bytesToSign);
96 | segments.Add(Base64UrlEncode(signature));
97 | }
98 |
99 | return string.Join(".", segments);
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Utils/Murmur3.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using IoStream = System.IO.Stream;
3 |
4 | namespace Stream.Utils
5 | {
6 | /*
7 | Copied from https://gist.githubusercontent.com/automatonic/3725443/raw/c2ffc51ed8e9ee3c89e8016c062672d3d52ef999/MurMurHash3.cs
8 | The only change is that we set the Seed value to zero to match the backend Go implementation.
9 | */
10 | internal static class Murmur3
11 | {
12 | // Change to suit your needs
13 | private const uint Seed = 0;
14 |
15 | internal static int Hash(IoStream stream)
16 | {
17 | const uint c1 = 0xcc9e2d51;
18 | const uint c2 = 0x1b873593;
19 |
20 | uint h1 = Seed;
21 | uint k1 = 0;
22 | uint streamLength = 0;
23 |
24 | using (BinaryReader reader = new BinaryReader(stream))
25 | {
26 | byte[] chunk = reader.ReadBytes(4);
27 | while (chunk.Length > 0)
28 | {
29 | streamLength += (uint)chunk.Length;
30 | switch (chunk.Length)
31 | {
32 | case 4:
33 | /* Get four bytes from the input into an uint */
34 | k1 = (uint)(chunk[0] | chunk[1] << 8 | chunk[2] << 16 | chunk[3] << 24);
35 |
36 | /* bitmagic hash */
37 | k1 *= c1;
38 | k1 = Rotl32(k1, 15);
39 | k1 *= c2;
40 |
41 | h1 ^= k1;
42 | h1 = Rotl32(h1, 13);
43 | h1 = (h1 * 5) + 0xe6546b64;
44 | break;
45 | case 3:
46 | k1 = (uint)(chunk[0] | chunk[1] << 8 | chunk[2] << 16);
47 | k1 *= c1;
48 | k1 = Rotl32(k1, 15);
49 | k1 *= c2;
50 | h1 ^= k1;
51 | break;
52 | case 2:
53 | k1 = (uint)(chunk[0] | chunk[1] << 8);
54 | k1 *= c1;
55 | k1 = Rotl32(k1, 15);
56 | k1 *= c2;
57 | h1 ^= k1;
58 | break;
59 | case 1:
60 | k1 = chunk[0];
61 | k1 *= c1;
62 | k1 = Rotl32(k1, 15);
63 | k1 *= c2;
64 | h1 ^= k1;
65 | break;
66 | }
67 |
68 | chunk = reader.ReadBytes(4);
69 | }
70 | }
71 |
72 | // finalization, magic chants to wrap it all up
73 | h1 ^= streamLength;
74 | h1 = Fmix(h1);
75 |
76 | // ignore overflow
77 | unchecked
78 | {
79 | return (int)h1;
80 | }
81 | }
82 |
83 | private static uint Rotl32(uint x, byte r)
84 | {
85 | return (x << r) | (x >> (32 - r));
86 | }
87 |
88 | private static uint Fmix(uint h)
89 | {
90 | h ^= h >> 16;
91 | h *= 0x85ebca6b;
92 | h ^= h >> 13;
93 | h *= 0xc2b2ae35;
94 | h ^= h >> 16;
95 | return h;
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/.stylecop.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/Rest/RestClient.cs:
--------------------------------------------------------------------------------
1 | using Stream.Utils;
2 | using System;
3 | using System.Net;
4 | using System.Net.Http;
5 | using System.Net.Http.Headers;
6 | using System.Text;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace Stream.Rest
11 | {
12 | internal class RestClient
13 | {
14 | private static readonly MediaTypeWithQualityHeaderValue _jsonAcceptHeader = new MediaTypeWithQualityHeaderValue("application/json");
15 | private static readonly HttpClient _client = new HttpClient();
16 | private readonly Uri _baseUrl;
17 | private readonly TimeSpan _timeout;
18 |
19 | internal RestClient(Uri baseUrl, TimeSpan timeout)
20 | {
21 | #if OLD_TLS_HANDLING
22 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
23 | #endif
24 | _baseUrl = baseUrl;
25 | _timeout = timeout;
26 | }
27 |
28 | private HttpRequestMessage BuildRequestMessage(HttpMethod method, Uri url, RestRequest request)
29 | {
30 | var requestMessage = new HttpRequestMessage(method, url);
31 | requestMessage.Headers.Accept.Add(_jsonAcceptHeader);
32 |
33 | request.Headers.ForEach(h =>
34 | {
35 | requestMessage.Headers.Add(h.Key, h.Value);
36 | });
37 |
38 | if (request.FileStream != null)
39 | {
40 | requestMessage.Content = CreateFileStream(request);
41 | }
42 | else if (method == HttpMethod.Post || method == HttpMethod.Put)
43 | {
44 | requestMessage.Content = new StringContent(request.JsonBody ?? "{}", Encoding.UTF8, "application/json");
45 | }
46 |
47 | return requestMessage;
48 | }
49 |
50 | private HttpContent CreateFileStream(RestRequest request)
51 | {
52 | var content = new MultipartFormDataContent();
53 | var streamContent = new StreamContent(request.FileStream);
54 |
55 | if (request.FileStreamContentType != null)
56 | {
57 | streamContent.Headers.Add("Content-Type", request.FileStreamContentType);
58 | }
59 |
60 | streamContent.Headers.Add("Content-Disposition", "form-data; name=\"file\"; filename=\"" + request.FileStreamName + "\"");
61 | content.Add(streamContent);
62 |
63 | return content;
64 | }
65 |
66 | private Uri BuildUri(RestRequest request)
67 | {
68 | var queryStringBuilder = new StringBuilder();
69 | request.QueryParameters.ForEach(p =>
70 | {
71 | queryStringBuilder.Append(queryStringBuilder.Length == 0 ? "?" : "&");
72 | queryStringBuilder.Append($"{p.Key}={Uri.EscapeDataString(p.Value)}");
73 | });
74 |
75 | return new Uri(_baseUrl, request.Resource + queryStringBuilder.ToString());
76 | }
77 |
78 | internal async Task ExecuteHttpRequestAsync(RestRequest request)
79 | {
80 | var uri = BuildUri(request);
81 |
82 | using (var cts = new CancellationTokenSource(_timeout))
83 | using (var requestMessage = BuildRequestMessage(request.Method, uri, request))
84 | {
85 | var response = await _client.SendAsync(requestMessage, cts.Token);
86 | return await RestResponse.FromResponseMessage(response);
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Models/ReactionOptions.cs:
--------------------------------------------------------------------------------
1 | using Stream.Rest;
2 | using System.Collections.Generic;
3 |
4 | namespace Stream.Models
5 | {
6 | public class ReactionOption
7 | {
8 | private int? _recentLimit;
9 |
10 | private OpType Type { get; set; }
11 |
12 | #pragma warning disable SA1300
13 | private enum OpType
14 | {
15 | own,
16 | recent,
17 | counts,
18 | ownChildren,
19 | }
20 | #pragma warning restore SA1300
21 |
22 | private readonly List _ops;
23 | private readonly List _kindFilters;
24 |
25 | private string _userId;
26 | private string _childrenUserId;
27 |
28 | private ReactionOption()
29 | {
30 | _ops = new List();
31 | _kindFilters = new List();
32 | }
33 |
34 | internal void Apply(RestRequest request)
35 | {
36 | _ops.ForEach(op =>
37 | {
38 | switch (op)
39 | {
40 | case OpType.own: request.AddQueryParameter("withOwnReactions", "true"); break;
41 | case OpType.recent: request.AddQueryParameter("withRecentReactions", "true"); break;
42 | case OpType.counts: request.AddQueryParameter("withReactionCounts", "true"); break;
43 | case OpType.ownChildren: request.AddQueryParameter("withOwnChildren", "true"); break;
44 | }
45 | });
46 | if (_recentLimit.HasValue)
47 | request.AddQueryParameter("recentReactionsLimit", _recentLimit.ToString());
48 |
49 | if (_kindFilters.Count != 0)
50 | request.AddQueryParameter("reactionKindsFilter", string.Join(",", _kindFilters));
51 |
52 | if (!string.IsNullOrWhiteSpace(_userId))
53 | request.AddQueryParameter("filter_user_id", _userId);
54 |
55 | if (!string.IsNullOrWhiteSpace(_childrenUserId))
56 | request.AddQueryParameter("children_user_id", _userId);
57 | }
58 |
59 | public static ReactionOption With()
60 | {
61 | return new ReactionOption();
62 | }
63 |
64 | public ReactionOption Own()
65 | {
66 | _ops.Add(OpType.own);
67 | return this;
68 | }
69 |
70 | public ReactionOption Recent()
71 | {
72 | _ops.Add(OpType.recent);
73 | return this;
74 | }
75 |
76 | public ReactionOption Counts()
77 | {
78 | _ops.Add(OpType.counts);
79 | return this;
80 | }
81 |
82 | public ReactionOption OwnChildren()
83 | {
84 | _ops.Add(OpType.ownChildren);
85 | return this;
86 | }
87 |
88 | public ReactionOption RecentLimit(int value)
89 | {
90 | _recentLimit = value;
91 | return this;
92 | }
93 |
94 | public ReactionOption KindFilter(string value)
95 | {
96 | _kindFilters.Add(value);
97 | return this;
98 | }
99 |
100 | public ReactionOption UserFilter(string value)
101 | {
102 | _userId = value;
103 | return this;
104 | }
105 |
106 | public ReactionOption ChildrenUserFilter(string value)
107 | {
108 | _childrenUserId = value;
109 | return this;
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/ICollections.cs:
--------------------------------------------------------------------------------
1 | using Stream.Models;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 |
5 | namespace Stream
6 | {
7 | ///
8 | /// Client to interact with collections.
9 | /// Collections enable you to store information to Stream. This allows you to use it inside your feeds,
10 | /// and to provide additional data for the personalized endpoints. Examples include products and articles,
11 | /// but any unstructured object (e.g. JSON) is a good match for collections.
12 | ///
13 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
14 | public interface ICollections
15 | {
16 | /// Creates a new collection.
17 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
18 | Task AddAsync(string collectionName, Dictionary data, string id = null, string userId = null);
19 |
20 | /// Deletes a collection.
21 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
22 | Task DeleteAsync(string collectionName, string id);
23 |
24 | /// Deletes multiple collections.
25 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
26 | Task DeleteManyAsync(string collectionName, IEnumerable ids);
27 |
28 | /// Returns a collection by id.
29 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
30 | Task GetAsync(string collectionName, string id);
31 |
32 | /// Returns a collection object.
33 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
34 | Task SelectAsync(string collectionName, string id);
35 |
36 | /// Returns multiple collection objects.
37 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
38 | Task SelectManyAsync(string collectionName, IEnumerable ids);
39 |
40 | /// Updates a specific collection object.
41 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
42 | Task UpdateAsync(string collectionName, string id, Dictionary data);
43 |
44 | /// Creates or updates a collection.
45 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
46 | Task UpsertAsync(string collectionName, CollectionObject data);
47 |
48 | /// Creates or updates multiple collections.
49 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/collections_introduction/?language=csharp
50 | Task UpsertManyAsync(string collectionName, IEnumerable data);
51 |
52 | /// Returns a reference identifier to the collection object.
53 | string Ref(string collectionName, string collectionObjectId);
54 |
55 | /// Returns a reference identifier to the collection object.
56 | string Ref(string collectionName, CollectionObject obj);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/IBatchOperations.cs:
--------------------------------------------------------------------------------
1 | using Stream.Models;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 |
5 | namespace Stream
6 | {
7 | ///
8 | /// Client to interact with batch operations.
9 | ///
10 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
11 | public interface IBatchOperations
12 | {
13 | /// Add an activity to multiple feeds.
14 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
15 | Task AddToManyAsync(Activity activity, IEnumerable feeds);
16 |
17 | /// Add an activity to multiple feeds.
18 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
19 | Task AddToManyAsync(Activity activity, IEnumerable feedIds);
20 |
21 | /// Follow muiltiple feeds.
22 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
23 | Task FollowManyAsync(IEnumerable follows, int activityCopyLimit = 100);
24 |
25 | /// Unfollow multiple feeds in a single request using UnfollowRelation objects.
26 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
27 | Task UnfollowManyAsync(IEnumerable unfollows);
28 |
29 | /// Get multiple activities by activity ids.
30 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
31 | Task> GetActivitiesByIdAsync(IEnumerable ids);
32 |
33 | /// Get multiple activities by foreign ids.
34 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
35 | Task> GetActivitiesByForeignIdAsync(IEnumerable ids);
36 |
37 | /// Get multiple enriched activities.
38 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
39 | Task> GetEnrichedActivitiesAsync(IEnumerable ids, GetOptions options = null);
40 |
41 | /// Get multiple enriched activities.
42 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
43 | Task> GetEnrichedActivitiesAsync(IEnumerable foreignIdTimes, GetOptions options = null);
44 |
45 | /// Updates multiple activities.
46 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
47 | Task UpdateActivitiesAsync(IEnumerable activities);
48 |
49 | /// Update a single activity.
50 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
51 | Task UpdateActivityAsync(Activity activity);
52 |
53 | /// Update multiple activities partially.
54 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/add_many_activities/?language=csharp
55 | Task ActivitiesPartialUpdateAsync(IEnumerable updates);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/ActivityTests/AggregateActivityTests.cs:
--------------------------------------------------------------------------------
1 | using NUnit.Framework;
2 | using Stream.Models;
3 | using System;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace StreamNetTests
8 | {
9 | [TestFixture]
10 | public class AggregateActivityTests : TestBase
11 | {
12 | [Test]
13 | public async Task TestAggregate()
14 | {
15 | var newActivity1 = new Activity("1", "test", "1");
16 | var newActivity2 = new Activity("1", "test", "2");
17 | var response = await UserFeed.AddActivityAsync(newActivity1);
18 | response = await UserFeed.AddActivityAsync(newActivity2);
19 |
20 | await AggregateFeed.FollowFeedAsync(this.UserFeed);
21 |
22 | var activities = (await this.AggregateFeed.GetAggregateActivitiesAsync()).Results;
23 | Assert.IsNotNull(activities);
24 | Assert.AreEqual(1, activities.Count());
25 |
26 | var aggActivity = activities.First() as AggregateActivity;
27 | Assert.IsNotNull(aggActivity);
28 | Assert.AreEqual(2, aggActivity.Activities.Count);
29 | Assert.AreEqual(1, aggActivity.ActorCount);
30 |
31 | await AggregateFeed.UnfollowFeedAsync(this.UserFeed);
32 | }
33 |
34 | [Test]
35 | public async Task TestMixedAggregate()
36 | {
37 | var newActivity1 = new Activity("1", "test", "1");
38 | var newActivity2 = new Activity("1", "test", "2");
39 | var newActivity3 = new Activity("1", "other", "2");
40 | var response = await UserFeed.AddActivityAsync(newActivity1);
41 | response = await UserFeed.AddActivityAsync(newActivity2);
42 | response = await UserFeed.AddActivityAsync(newActivity3);
43 |
44 | await AggregateFeed.FollowFeedAsync(this.UserFeed);
45 |
46 | var activities = (await this.AggregateFeed.GetAggregateActivitiesAsync(null)).Results;
47 | Assert.IsNotNull(activities);
48 | Assert.AreEqual(2, activities.Count());
49 |
50 | var aggActivity = activities.First() as AggregateActivity;
51 | Assert.IsNotNull(aggActivity);
52 | Assert.AreEqual(1, aggActivity.Activities.Count);
53 | Assert.AreEqual(1, aggActivity.ActorCount);
54 |
55 | await AggregateFeed.UnfollowFeedAsync(this.UserFeed);
56 | }
57 |
58 | [Test]
59 | public async Task TestGetAggregate()
60 | {
61 | var newActivity1 = new Activity("1", "test", "1");
62 | var newActivity2 = new Activity("1", "test", "2");
63 | await UserFeed.AddActivityAsync(newActivity1);
64 | await UserFeed.AddActivityAsync(newActivity2);
65 |
66 | await AggregateFeed.FollowFeedAsync(this.UserFeed);
67 |
68 | var result = await this.AggregateFeed.GetAggregateActivitiesAsync();
69 | var activities = result.Results;
70 | Assert.IsNotNull(activities);
71 | Assert.AreEqual(1, activities.Count());
72 |
73 | var aggActivity = activities.First();
74 | Assert.IsNotNull(aggActivity);
75 | Assert.AreEqual(2, aggActivity.Activities.Count);
76 | Assert.AreEqual(1, aggActivity.ActorCount);
77 | Assert.IsNotNull(aggActivity.CreatedAt);
78 | Assert.IsTrue(Math.Abs(aggActivity.CreatedAt.Value.Subtract(DateTime.UtcNow).TotalMinutes) < 10);
79 | Assert.IsNotNull(aggActivity.UpdatedAt);
80 | Assert.IsTrue(Math.Abs(aggActivity.UpdatedAt.Value.Subtract(DateTime.UtcNow).TotalMinutes) < 10);
81 | Assert.IsNotNull(aggActivity.Group);
82 |
83 | await AggregateFeed.UnfollowFeedAsync(this.UserFeed);
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/src/IStreamClient.cs:
--------------------------------------------------------------------------------
1 | using Stream.Models;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Threading.Tasks;
5 |
6 | namespace Stream
7 | {
8 | /// Base client for the Stream API.
9 | public interface IStreamClient
10 | {
11 | ///
12 | /// Returns an instance that let's you interact with batch operations.
13 | /// You can used the returned instance as a singleton in your application.
14 | ///
15 | IBatchOperations Batch { get; }
16 |
17 | ///
18 | /// Returns an instance that let's you interact with collections.
19 | /// You can used the returned instance as a singleton in your application.
20 | ///
21 | ICollections Collections { get; }
22 |
23 | ///
24 | /// Returns an instance that let's you interact with UsersBatch.
25 | /// You can used the returned instance as a singleton in your application.
26 | ///
27 | IUsersBatch UsersBatch { get; }
28 |
29 | ///
30 | /// Returns an instance that let's you interact with reactions.
31 | /// You can used the returned instance as a singleton in your application.
32 | ///
33 | IReactions Reactions { get; }
34 |
35 | IModeration Moderation { get; }
36 |
37 | ///
38 | /// Returns an instance that let's you interact with users.
39 | /// You can used the returned instance as a singleton in your application.
40 | ///
41 | IUsers Users { get; }
42 |
43 | ///
44 | /// Returns an instance that let's you interact with personalizations.
45 | /// You can used the returned instance as a singleton in your application.
46 | ///
47 | IPersonalization Personalization { get; }
48 |
49 | ///
50 | /// Returns an instance that let's you interact with files.
51 | /// You can used the returned instance as a singleton in your application.
52 | ///
53 | IFiles Files { get; }
54 |
55 | ///
56 | /// Returns an instance that let's you interact with images.
57 | /// You can used the returned instance as a singleton in your application.
58 | ///
59 | IImages Images { get; }
60 |
61 | ///
62 | /// Partial update of an .
63 | ///
64 | Task ActivityPartialUpdateAsync(string id = null, ForeignIdTime foreignIdTime = null, Dictionary set = null, IEnumerable unset = null);
65 |
66 | ///
67 | /// Returns an instance that let's you interact with feeds.
68 | ///
69 | IStreamFeed Feed(string feedSlug, string userId);
70 |
71 | ///
72 | /// Reads enriched activities of a personalized feed.
73 | ///
74 | Task> GetPersonalizedFeedAsync(GetOptions options = null);
75 |
76 | ///
77 | /// Allows you to retrieve open graph information from a URL which you can then use to add images and a description to activities.
78 | ///
79 | Task OgAsync(string url);
80 |
81 | ///
82 | /// Creates a JWT for the given .
83 | ///
84 | string CreateUserToken(string userId, IDictionary extraData = null);
85 | }
86 | }
--------------------------------------------------------------------------------
/src/Models/Reaction.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Newtonsoft.Json.Linq;
3 | using Stream.Rest;
4 | using System;
5 | using System.Collections.Generic;
6 | using ReactionFilter = Stream.Models.FeedFilter;
7 |
8 | namespace Stream.Models
9 | {
10 | public class Reaction
11 | {
12 | public string Id { get; set; }
13 | public string Kind { get; set; }
14 | public DateTime? CreatedAt { get; set; }
15 | public DateTime? UpdatedAt { get; set; }
16 | public string ActivityId { get; set; }
17 | public string UserId { get; set; }
18 | public string ModerationTemplate { get; set; }
19 | public GenericData User { get; set; }
20 |
21 | public IDictionary Data { get; set; }
22 | public IEnumerable TargetFeeds { get; set; }
23 |
24 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "parent")]
25 | public string ParentId { get; set; }
26 |
27 | public Dictionary LatestChildren { get; set; }
28 |
29 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "children_counts")]
30 | public Dictionary ChildrenCounters { get; set; }
31 |
32 | public DateTime? DeletedAt { get; set; }
33 |
34 | public string Ref() => $"SR:{Id}";
35 |
36 | public Dictionary Moderation { get; set; }
37 | public ModerationResponse GetModerationResponse()
38 | {
39 | var key = "response";
40 | if (Moderation != null && Moderation.ContainsKey(key))
41 | {
42 | return ((JObject)Moderation[key]).ToObject();
43 | }
44 | else
45 | {
46 | throw new KeyNotFoundException($"Key '{key}' not found in moderation dictionary.");
47 | }
48 | }
49 | }
50 |
51 | public class ReactionsWithActivity : GenericGetResponse
52 | {
53 | public EnrichedActivity Activity { get; set; }
54 | }
55 |
56 | internal class ReactionsFilterResponse : GenericGetResponse
57 | {
58 | }
59 |
60 | public class ReactionFiltering
61 | {
62 | private const int DefaultLimit = 10;
63 | private int _limit = DefaultLimit;
64 | private ReactionFilter _filter = null;
65 |
66 | public ReactionFiltering WithLimit(int limit)
67 | {
68 | _limit = limit;
69 | return this;
70 | }
71 |
72 | public ReactionFiltering WithFilter(ReactionFilter filter)
73 | {
74 | _filter = filter;
75 | return this;
76 | }
77 |
78 | internal ReactionFiltering WithActivityData()
79 | {
80 | _filter = _filter == null ? ReactionFilter.Where().WithActivityData() : _filter.WithActivityData();
81 | return this;
82 | }
83 |
84 | internal void Apply(RestRequest request)
85 | {
86 | request.AddQueryParameter("limit", _limit.ToString());
87 | _filter?.Apply(request);
88 | }
89 |
90 | internal bool IncludesActivityData => _filter.IncludesActivityData;
91 |
92 | public static ReactionFiltering Default => new ReactionFiltering().WithLimit(DefaultLimit);
93 | }
94 |
95 | public class ReactionPagination
96 | {
97 | private string _kind;
98 | private string _lookupAttr;
99 | private string _lookupValue;
100 |
101 | public ReactionPagination ActivityId(string activityId)
102 | {
103 | _lookupAttr = "activity_id";
104 | _lookupValue = activityId;
105 | return this;
106 | }
107 |
108 | public ReactionPagination ReactionId(string reactionId)
109 | {
110 | _lookupAttr = "reaction_id";
111 | _lookupValue = reactionId;
112 | return this;
113 | }
114 |
115 | public ReactionPagination UserId(string userId)
116 | {
117 | _lookupAttr = "user_id";
118 | _lookupValue = userId;
119 | return this;
120 | }
121 |
122 | public ReactionPagination Kind(string kind)
123 | {
124 | _kind = kind;
125 | return this;
126 | }
127 |
128 | public static ReactionPagination By { get => new ReactionPagination(); }
129 |
130 | public string GetPath()
131 | => _kind == null ? $"{_lookupAttr}/{_lookupValue}/" : $"{_lookupAttr}/{_lookupValue}/{_kind}/";
132 | }
133 | }
--------------------------------------------------------------------------------
/src/IReactions.cs:
--------------------------------------------------------------------------------
1 | using Stream.Models;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 |
5 | namespace Stream
6 | {
7 | ///
8 | /// Client to interact with reactions.
9 | /// Reactions are a special kind of data that can be used to capture user interaction with specific activities.
10 | /// Common examples of reactions are likes, comments, and upvotes. Reactions are automatically returned to feeds'
11 | /// activities at read time when the reactions parameters are used.
12 | ///
13 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
14 | public interface IReactions
15 | {
16 | /// Posts a new reaciton.
17 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
18 | Task AddAsync(string reactionId, string kind, string activityId, string userId, IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null);
19 |
20 | /// Posts a new reaciton.
21 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
22 | Task AddAsync(string kind, string activityId, string userId, IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null);
23 |
24 | /// Adds a new child reaction.
25 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
26 | Task AddChildAsync(string parentId, string reactionId, string kind, string userId, IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null);
27 |
28 | /// Adds a new child reaction.
29 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
30 | Task AddChildAsync(Reaction parent, string reactionId, string kind, string userId, IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null);
31 |
32 | /// Adds a new child reaction.
33 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
34 | Task AddChildAsync(string parentId, string kind, string userId, IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null);
35 |
36 | /// Adds a new child reaction.
37 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
38 | Task AddChildAsync(Reaction parent, string kind, string userId, IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null);
39 |
40 | /// Deletes a reactions.
41 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
42 | Task DeleteAsync(string reactionId, bool soft = false);
43 |
44 | /// Restores a soft deleted reaction.
45 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
46 | Task RestoreSoftDeletedAsync(string reactionId);
47 |
48 | /// Retrieves reactions.
49 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
50 | Task> FilterAsync(ReactionFiltering filtering, ReactionPagination pagination);
51 |
52 | /// Retrieves reactions and its' activities.
53 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
54 | Task FilterWithActivityAsync(ReactionFiltering filtering, ReactionPagination pagination);
55 |
56 | /// Retrieves a single reaction.
57 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
58 | Task GetAsync(string reactionId);
59 |
60 | ///