├── .github ├── CODEOWNERS └── workflows │ ├── skip-ci-on-release.yml │ ├── reviewdog.yml │ ├── ci.yaml │ ├── initiate_release.yaml │ └── release.yaml ├── .DS_Store ├── assets ├── nuget_logo.png └── logo.svg ├── tests ├── helloworld.jpg ├── helloworld.txt ├── Extensions.cs ├── Credentials.cs ├── FeedIdValidatorTests.cs ├── OpenGraphTests.cs ├── stream-net-tests.csproj ├── TestBase.cs ├── FileTests.cs ├── PersonalizationTests.cs ├── ActivityTests │ ├── RemoveActivityTests.cs │ ├── AggregateActivityTests.cs │ └── NotificationActivityTests.cs ├── FeedTests.cs ├── UserTests.cs ├── UtilsTests.cs ├── UsersBatchTests.cs ├── ClientTests.cs ├── ModerationTests.cs └── ReactionTests.cs ├── src ├── Models │ ├── Upload.cs │ ├── Follower.cs │ ├── ResponseBase.cs │ ├── UpdateToTargetsResponse.cs │ ├── ForeignIdTime.cs │ ├── User.cs │ ├── FollowStats.cs │ ├── Follow.cs │ ├── UsersBatch.cs │ ├── EnrichedActivity.cs │ ├── UpdateToTargets.cs │ ├── GenericResponse.cs │ ├── AggregateActivity.cs │ ├── NotificationActivity.cs │ ├── Og.cs │ ├── GenericData.cs │ ├── UnfollowRelation.cs │ ├── ActivityMarker.cs │ ├── Activity.cs │ ├── FeedFilter.cs │ ├── CollectionObject.cs │ ├── CustomDataBase.cs │ ├── ReactionOptions.cs │ ├── Reaction.cs │ └── GetOptions.cs ├── IUsersBatch.cs ├── Rest │ ├── RestResponse.cs │ ├── RestRequest.cs │ └── RestClient.cs ├── Utils │ ├── Extensions.cs │ ├── FeedIdValidator.cs │ ├── StreamJsonConverter.cs │ ├── Murmur3.cs │ └── ActivityIdGenerator.cs ├── IModeration.cs ├── IImages.cs ├── IFiles.cs ├── Images.cs ├── IPersonalization.cs ├── Files.cs ├── IUsers.cs ├── StreamClientOptions.cs ├── Moderation.cs ├── Personalization.cs ├── stream-net.csproj ├── UsersBatch.cs ├── Users.cs ├── StreamException.cs ├── StreamClientToken.cs ├── ICollections.cs ├── IBatchOperations.cs ├── IStreamClient.cs ├── IReactions.cs ├── Collections.cs ├── IStreamFeed.cs └── Reactions.cs ├── .gitignore ├── samples └── ConsoleApp │ ├── ConsoleApp.csproj │ ├── README.md │ └── Program.cs ├── scripts ├── get_changelog_diff.js └── create_release_branch.sh ├── SECURITY.md ├── LICENSE ├── stream-net.sln ├── CONTRIBUTING.md └── .stylecop.ruleset /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @xernobyl @JimmyPettersson85 @itsmeadi 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-net/HEAD/.DS_Store -------------------------------------------------------------------------------- /assets/nuget_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-net/HEAD/assets/nuget_logo.png -------------------------------------------------------------------------------- /tests/helloworld.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-net/HEAD/tests/helloworld.jpg -------------------------------------------------------------------------------- /tests/helloworld.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-net/HEAD/tests/helloworld.txt -------------------------------------------------------------------------------- /src/Models/Upload.cs: -------------------------------------------------------------------------------- 1 | namespace Stream.Models 2 | { 3 | public class Upload 4 | { 5 | public string File { get; set; } 6 | public string Duration { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Models/Follower.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Stream.Models 4 | { 5 | public class Follower 6 | { 7 | public string FeedId { get; set; } 8 | public string TargetId { get; set; } 9 | public DateTime CreatedAt { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Models/ResponseBase.cs: -------------------------------------------------------------------------------- 1 | namespace Stream.Models 2 | { 3 | /// Base class for all API responses. 4 | public class ResponseBase 5 | { 6 | /// Duration of the request in human-readable format. 7 | public string Duration { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #ignores 2 | *.suo 3 | *.csproj.user 4 | *.user 5 | bin 6 | obj 7 | *.mdf 8 | *.ldf 9 | *.gitattributes 10 | *.log 11 | Thumbs.db 12 | .idea/ 13 | *packages/ 14 | *node_modules/ 15 | *App_Data/ 16 | *PublishProfiles/ 17 | nuget.exe 18 | *.nupkg 19 | *TestResults/ 20 | .vs/ 21 | .vscode/ 22 | .envrc 23 | .runsettings 24 | -------------------------------------------------------------------------------- /samples/ConsoleApp/ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Exe 9 | net6.0 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Models/UpdateToTargetsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Stream.Models 4 | { 5 | public class UpdateToTargetsResponse 6 | { 7 | public Activity Activity { get; set; } 8 | public List Added { get; set; } 9 | public List Removed { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Models/ForeignIdTime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Stream.Models 4 | { 5 | public class ForeignIdTime 6 | { 7 | public string ForeignId { get; set; } 8 | public DateTime Time { get; set; } 9 | 10 | public ForeignIdTime(string foreignId, DateTime time) 11 | { 12 | ForeignId = foreignId; 13 | Time = time; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace StreamNetTests 5 | { 6 | internal static class Extensions 7 | { 8 | internal static int CountOrFallback(this IEnumerable list, int fallbackValue = 0) 9 | { 10 | if (list == null) 11 | return fallbackValue; 12 | 13 | return list.Count(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/IUsersBatch.cs: -------------------------------------------------------------------------------- 1 | using Stream.Models; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Stream 6 | { 7 | public interface IUsersBatch 8 | { 9 | Task> UpsertUsersAsync(IEnumerable users, bool overrideExisting = false); 10 | Task> GetUsersAsync(IEnumerable userIds); 11 | Task> DeleteUsersAsync(IEnumerable userIds); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Stream 5 | { 6 | public class User 7 | { 8 | public string Id { get; set; } 9 | public DateTime? CreatedAt { get; set; } 10 | public DateTime? UpdatedAt { get; set; } 11 | public IDictionary Data { get; set; } 12 | 13 | /// Returns a reference identifier to this object. 14 | public string Ref() => $"SU:{Id}"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Models/FollowStats.cs: -------------------------------------------------------------------------------- 1 | namespace Stream.Models 2 | { 3 | public class FollowStat 4 | { 5 | public int Count { get; set; } 6 | public string Feed { get; set; } 7 | } 8 | 9 | public class FollowStats 10 | { 11 | public FollowStat Followers { get; set; } 12 | public FollowStat Following { get; set; } 13 | } 14 | 15 | public class FollowStatsResponse : ResponseBase 16 | { 17 | public FollowStats Results { get; set; } 18 | } 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Models/Follow.cs: -------------------------------------------------------------------------------- 1 | namespace Stream.Models 2 | { 3 | public class Follow 4 | { 5 | public string Source { get; set; } 6 | public string Target { get; set; } 7 | 8 | public Follow(string source, string target) 9 | { 10 | Source = source; 11 | Target = target; 12 | } 13 | 14 | public Follow(IStreamFeed source, IStreamFeed target) 15 | { 16 | Source = source.FeedId; 17 | Target = target.FeedId; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/skip-ci-on-release.yml: -------------------------------------------------------------------------------- 1 | # This workflow skips all heavy stuff for release branches 2 | name: Skip Tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - 'release-**' 8 | tags-ignore: 9 | - '**' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.head_ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | reviewdog: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - run: 'echo "Not required"' 20 | 21 | basic: 22 | name: Run tests 23 | runs-on: ubuntu-latest 24 | steps: 25 | - run: 'echo "Not required"' 26 | -------------------------------------------------------------------------------- /src/Models/UsersBatch.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 AddUserBatchResponse 10 | { 11 | public IEnumerable CreatedUsers { get; set; } 12 | } 13 | 14 | public class GetUserBatchResponse 15 | { 16 | public IEnumerable Users { get; set; } 17 | } 18 | 19 | public class DeleteUsersBatchResponse 20 | { 21 | public IEnumerable DeletedUserIds { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Models/EnrichedActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Stream.Models 4 | { 5 | public class EnrichedActivity : ActivityBase 6 | { 7 | public GenericData Actor { get; set; } 8 | public GenericData Object { get; set; } 9 | public GenericData Target { get; set; } 10 | public GenericData Origin { get; set; } 11 | public Dictionary ReactionCounts { get; set; } 12 | public Dictionary> OwnReactions { get; set; } 13 | public Dictionary> LatestReactions { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Credentials.cs: -------------------------------------------------------------------------------- 1 | using Stream; 2 | using System; 3 | 4 | namespace StreamNetTests 5 | { 6 | public class Credentials 7 | { 8 | internal Credentials() 9 | { 10 | Client = new StreamClient( 11 | Environment.GetEnvironmentVariable("STREAM_API_KEY"), 12 | Environment.GetEnvironmentVariable("STREAM_API_SECRET"), 13 | new StreamClientOptions 14 | { 15 | Location = StreamApiLocation.USEast, 16 | Timeout = 16000, 17 | }); 18 | } 19 | 20 | public static Credentials Instance { get; } = new Credentials(); 21 | public StreamClient Client { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /tests/FeedIdValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Stream.Utils; 3 | 4 | public class FeedIdValidatorTests 5 | { 6 | [Test] 7 | public void TestFeedIdValidator() 8 | { 9 | var invalidFeedIds = new[] { "nocolon", ":beginning", "ending:", "mul:tip:le:colons" }; 10 | var valifFeedIds = new[] { "flat:myfeedname" }; 11 | 12 | foreach (var feedId in invalidFeedIds) 13 | { 14 | Assert.Throws(() => FeedIdValidator.ThrowIfFeedIdIsInvalid(feedId)); 15 | } 16 | 17 | foreach (var feedId in valifFeedIds) 18 | { 19 | Assert.DoesNotThrow(() => FeedIdValidator.ThrowIfFeedIdIsInvalid(feedId)); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Rest/RestResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace Stream.Rest 6 | { 7 | internal class RestResponse 8 | { 9 | internal HttpStatusCode StatusCode { get; set; } 10 | 11 | internal string Content { get; set; } 12 | 13 | internal static async Task FromResponseMessage(HttpResponseMessage message) 14 | { 15 | var response = new RestResponse { StatusCode = message.StatusCode }; 16 | 17 | using (message) 18 | { 19 | response.Content = await message.Content.ReadAsStringAsync(); 20 | } 21 | 22 | return response; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Utils/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Stream.Utils 6 | { 7 | internal static class Extensions 8 | { 9 | internal static void ForEach(this IEnumerable items, Action action) 10 | { 11 | if (items == null || action == null) 12 | return; 13 | 14 | foreach (var item in items) 15 | action(item); 16 | } 17 | 18 | internal static int CountOrFallback(this IEnumerable list, int fallbackValue = 0) 19 | { 20 | if (list == null) 21 | return fallbackValue; 22 | 23 | return list.Count(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/OpenGraphTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Stream; 3 | using System.Threading.Tasks; 4 | 5 | namespace StreamNetTests 6 | { 7 | [TestFixture] 8 | public class OpenGraphTests : TestBase 9 | { 10 | [Test] 11 | public async Task TestOG() 12 | { 13 | var og = await Client.OgAsync("https://getstream.io/blog/try-out-the-stream-api-with-postman"); 14 | 15 | Assert.IsNotEmpty(og.Type); 16 | Assert.IsNotEmpty(og.Title); 17 | Assert.IsNotEmpty(og.Description); 18 | Assert.IsNotEmpty(og.Url); 19 | Assert.IsNotEmpty(og.Favicon); 20 | Assert.IsNotEmpty(og.Images); 21 | Assert.IsNotEmpty(og.Images[0].Image); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /scripts/get_changelog_diff.js: -------------------------------------------------------------------------------- 1 | /* 2 | Here we're trying to parse the latest changes from CHANGELOG.md file. 3 | The changelog looks like this: 4 | 5 | ## 0.0.3 6 | - Something #3 7 | ## 0.0.2 8 | - Something #2 9 | ## 0.0.1 10 | - Something #1 11 | 12 | In this case we're trying to extract "- Something #3" since that's the latest change. 13 | */ 14 | module.exports = () => { 15 | const fs = require('fs') 16 | 17 | changelog = fs.readFileSync('CHANGELOG.md', 'utf8') 18 | releases = changelog.match(/## [?[0-9](.+)/g) 19 | 20 | current_release = changelog.indexOf(releases[0]) 21 | previous_release = changelog.indexOf(releases[1]) 22 | 23 | latest_changes = changelog.substr(current_release, previous_release-current_release) 24 | 25 | return latest_changes 26 | } 27 | -------------------------------------------------------------------------------- /src/Models/UpdateToTargets.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace Stream.Models 6 | { 7 | public class UpdateToTargetsRequest 8 | { 9 | [JsonProperty("foreign_id")] 10 | public string ForeignID { get; set; } 11 | 12 | [JsonProperty("time")] 13 | public string Time { get; set; } 14 | 15 | [JsonProperty("id")] 16 | public string Id { get; set; } 17 | 18 | [JsonProperty("new_targets")] 19 | public List NewTargets { get; set; } 20 | 21 | [JsonProperty("added_targets")] 22 | public List Adds { get; set; } 23 | 24 | [JsonProperty("removed_targets")] 25 | public List RemovedTargets { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Models/GenericResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Stream.Models 4 | { 5 | /// Base class for read responses of . 6 | public class GenericGetResponse : ResponseBase 7 | { 8 | /// Container for objects. 9 | public List Results { get; set; } 10 | } 11 | 12 | /// Base class for personalized read responses of . 13 | public class PersonalizedGetResponse : GenericGetResponse 14 | { 15 | public int Limit { get; set; } 16 | public string Next { get; set; } 17 | public int Offset { get; set; } 18 | public string Version { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/IModeration.cs: -------------------------------------------------------------------------------- 1 | using Stream.Models; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Stream 6 | { 7 | public interface IModeration 8 | { 9 | Task FlagUserAsync(string flaggingUserID, string flaggedUserId, string reason, IDictionary options = null); 10 | 11 | Task FlagActivityAsync(string entityId, string entityCreatorId, string reason, 12 | IDictionary options = null); 13 | 14 | Task FlagReactionAsync(string entityId, string entityCreatorId, string reason, 15 | IDictionary options = null); 16 | Task FlagAsync(string entityType, string entityId, string entityCreatorId, string reason, IDictionary options = null); 17 | } 18 | } -------------------------------------------------------------------------------- /src/IImages.cs: -------------------------------------------------------------------------------- 1 | using Stream.Models; 2 | using System.Threading.Tasks; 3 | 4 | namespace Stream 5 | { 6 | /// Client to interact with images. 7 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/files_introduction/?language=csharp 8 | public interface IImages 9 | { 10 | /// Delete an image using it's URL. 11 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/files_introduction/?language=csharp 12 | Task DeleteAsync(string url); 13 | 14 | /// Upload an image. 15 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/files_introduction/?language=csharp 16 | Task UploadAsync(System.IO.Stream image, string name, string contentType); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Models/AggregateActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Stream.Models 5 | { 6 | public abstract class AggregateActivityBase 7 | { 8 | public string Id { get; set; } 9 | public string Group { get; set; } 10 | public string Verb { get; set; } 11 | public int ActorCount { get; set; } 12 | public int ActivityCount { get; set; } 13 | public DateTime? CreatedAt { get; set; } 14 | public DateTime? UpdatedAt { get; set; } 15 | } 16 | 17 | public class AggregateActivity : AggregateActivityBase 18 | { 19 | public List Activities { get; set; } 20 | } 21 | 22 | public class EnrichedAggregateActivity : AggregateActivityBase 23 | { 24 | public List Activities { get; set; } 25 | public int Score { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yml: -------------------------------------------------------------------------------- 1 | name: reviewdog 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | - '!release-**' 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | reviewdog: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - uses: reviewdog/action-setup@v1 19 | with: 20 | reviewdog_version: latest 21 | 22 | - name: Setup dotnet 23 | uses: actions/setup-dotnet@v2 24 | with: 25 | dotnet-version: 6.0.x 26 | 27 | - name: Reviewdog 28 | env: 29 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: | 31 | dotnet build -clp:NoSummary -p:GenerateFullPaths=true --no-incremental --nologo -f net6.0 -v q src \ 32 | | reviewdog -f=dotnet -name=dotnet -reporter=github-pr-review -------------------------------------------------------------------------------- /scripts/create_release_branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$1 4 | echo "Preparing release $VERSION" 5 | 6 | # Update .csproj file 7 | # This regex to update Version tag in .csproj file 8 | sed -i '' 's|\(.*\)|'"${VERSION}"'|g' src/stream-net.csproj 9 | 10 | 11 | # Create changelog 12 | # --skip.commit: We manually commit the changes 13 | # --skip-tag: tagging will done by the GitHub release step, so skip it here 14 | # --tag-prefix: by default it tries to compare v0.1.0 to v0.2.0. Since we do not prefix our tags with 'v' 15 | # we set it to an empty string 16 | npx --yes standard-version@9.3.2 --release-as "$VERSION" --skip.tag --skip.commit --tag-prefix= 17 | 18 | git config --global user.name 'github-actions' 19 | git config --global user.email 'release@getstream.io' 20 | git checkout -q -b "release-$VERSION" 21 | git commit -am "chore(release): $VERSION" 22 | git push -q -u origin "release-$VERSION" 23 | 24 | echo "Done!" 25 | -------------------------------------------------------------------------------- /tests/stream-net-tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | TRACE;DEBUG;NETCORE 4 | 5 | 6 | net8.0 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | stream-net 18 | 19 | 20 | 21 | ../.stylecop.ruleset 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Models/NotificationActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Stream.Models 4 | { 5 | public abstract class NotificationActivityBase : AggregateActivityBase 6 | { 7 | /// Whether the activity was read. 8 | public bool IsRead { get; set; } 9 | 10 | /// Whether he activity was seen. 11 | public bool IsSeen { get; set; } 12 | } 13 | 14 | public class NotificationActivity : NotificationActivityBase 15 | { 16 | public List Activities { get; set; } 17 | } 18 | 19 | public class EnrichedNotificationActivity : NotificationActivityBase 20 | { 21 | public List Activities { get; set; } 22 | } 23 | 24 | public class NotificationGetResponse : GenericGetResponse 25 | { 26 | public int Unseen { get; set; } 27 | public int Unread { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | At Stream we are committed to the security of our Software. We appreciate your efforts in disclosing vulnerabilities responsibly and we will make every effort to acknowledge your contributions. 3 | 4 | Report security vulnerabilities at the following email address: 5 | ``` 6 | [security@getstream.io](mailto:security@getstream.io) 7 | ``` 8 | Alternatively it is also possible to open a new issue in the affected repository, tagging it with the `security` tag. 9 | 10 | A team member will acknowledge the vulnerability and will follow-up with more detailed information. A representative of the security team will be in touch if more information is needed. 11 | 12 | # Information to include in a report 13 | While we appreciate any information that you are willing to provide, please make sure to include the following: 14 | * Which repository is affected 15 | * Which branch, if relevant 16 | * Be as descriptive as possible, the team will replicate the vulnerability before working on a fix. 17 | -------------------------------------------------------------------------------- /src/IFiles.cs: -------------------------------------------------------------------------------- 1 | using Stream.Models; 2 | using System.Threading.Tasks; 3 | 4 | namespace Stream 5 | { 6 | /// Client to interact with files. 7 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/files_introduction/?language=csharp 8 | public interface IFiles 9 | { 10 | /// Deletes a file using it's URL. 11 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/files_introduction/?language=csharp 12 | Task DeleteAsync(string url); 13 | 14 | /// Uploads a file. 15 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/files_introduction/?language=csharp 16 | Task UploadAsync(System.IO.Stream file, string name); 17 | 18 | /// Uploads a file. The field is attached as Content-Type header to the request. 19 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/files_introduction/?language=csharp 20 | Task UploadAsync(System.IO.Stream file, string name, string contentType); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | - '!release-**' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | basic: 15 | name: Run tests 16 | runs-on: ubuntu-latest 17 | env: 18 | DOTNET_CLI_TELEMETRY_OPTOUT: "true" 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | 24 | # - uses: wagoid/commitlint-github-action@v4 25 | 26 | - name: Setup dotnet 27 | uses: actions/setup-dotnet@v2 28 | with: 29 | dotnet-version: 8.0.x 30 | 31 | - name: Dependency cache 32 | uses: actions/cache@v3 33 | id: cache 34 | with: 35 | path: ~/.nuget/packages 36 | key: ${{ runner.os }}-nuget-${{ hashFiles('./**/*.csproj') }} 37 | 38 | - name: Install dependencies 39 | if: steps.cache.outputs.cache-hit != 'true' 40 | run: dotnet restore 41 | 42 | - name: Run tests 43 | run: dotnet test 44 | env: 45 | STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} 46 | STREAM_API_KEY: ${{ secrets.STREAM_API_KEY }} 47 | -------------------------------------------------------------------------------- /tests/TestBase.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Stream; 3 | 4 | namespace StreamNetTests 5 | { 6 | public abstract class TestBase 7 | { 8 | protected IStreamClient Client { get; private set; } 9 | protected IStreamFeed UserFeed { get; private set; } 10 | protected IStreamFeed UserFeed2 { get; private set; } 11 | protected IStreamFeed RankedFeed { get; private set; } 12 | protected IStreamFeed FlatFeed { get; private set; } 13 | protected IStreamFeed AggregateFeed { get; private set; } 14 | protected IStreamFeed NotificationFeed { get; private set; } 15 | 16 | [SetUp] 17 | public void Setup() 18 | { 19 | Client = Credentials.Instance.Client; 20 | UserFeed = Client.Feed("user", System.Guid.NewGuid().ToString()); 21 | UserFeed2 = Client.Feed("user", System.Guid.NewGuid().ToString()); 22 | FlatFeed = Client.Feed("flat", System.Guid.NewGuid().ToString()); 23 | AggregateFeed = Client.Feed("aggregate", System.Guid.NewGuid().ToString()); 24 | NotificationFeed = Client.Feed("notification", System.Guid.NewGuid().ToString()); 25 | RankedFeed = Client.Feed("ranked", System.Guid.NewGuid().ToString()); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /tests/FileTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Stream.Models; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | namespace StreamNetTests 7 | { 8 | [TestFixture] 9 | public class FileTests : TestBase 10 | { 11 | [Test] 12 | public async Task TestUpload() 13 | { 14 | Upload upload; 15 | 16 | using (var fs = File.OpenRead("../../../helloworld.txt")) 17 | { 18 | upload = await Client.Files.UploadAsync(fs, "helloworld.txt"); 19 | Assert.IsNotEmpty(upload.File); 20 | 21 | await Client.Files.DeleteAsync(upload.File); 22 | } 23 | 24 | using (var fs = File.OpenRead("../../../helloworld.txt")) 25 | { 26 | upload = await Client.Files.UploadAsync(fs, "helloworld.txt", "text/plain"); 27 | Assert.IsNotEmpty(upload.File); 28 | 29 | await Client.Files.DeleteAsync(upload.File); 30 | } 31 | 32 | using (var fs = File.OpenRead(@"../../../helloworld.jpg")) 33 | { 34 | upload = await Client.Images.UploadAsync(fs, "helloworld.jpg", "image/jpeg"); 35 | Assert.IsNotEmpty(upload.File); 36 | 37 | await Client.Images.DeleteAsync(upload.File); 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /.github/workflows/initiate_release.yaml: -------------------------------------------------------------------------------- 1 | name: Create release PR 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'The new version number. Example: 1.40.1' 8 | required: true 9 | 10 | jobs: 11 | init_release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 # gives the changelog generator access to all previous commits 17 | 18 | - name: Create release branch 19 | run: scripts/create_release_branch.sh "${{ github.event.inputs.version }}" 20 | 21 | - name: Get changelog diff 22 | uses: actions/github-script@v6 23 | with: 24 | script: | 25 | const get_change_log_diff = require('./scripts/get_changelog_diff.js') 26 | core.exportVariable('CHANGELOG', get_change_log_diff()) 27 | 28 | - name: Open pull request 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | run: | 32 | gh pr create \ 33 | -t "chore: release ${{ github.event.inputs.version }}" \ 34 | -b "# :rocket: ${{ github.event.inputs.version }} 35 | Make sure to use squash & merge when merging! 36 | Once this is merged, another job will kick off automatically and publish the package. 37 | # :memo: Changelog 38 | ${{ env.CHANGELOG }}" 39 | -------------------------------------------------------------------------------- /src/Images.cs: -------------------------------------------------------------------------------- 1 | using Stream.Models; 2 | using Stream.Utils; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace Stream 8 | { 9 | public class Images : IImages 10 | { 11 | private readonly StreamClient _client; 12 | 13 | internal Images(StreamClient client) 14 | { 15 | _client = client; 16 | } 17 | 18 | public async Task UploadAsync(System.IO.Stream image, string name, string contentType) 19 | { 20 | var request = _client.BuildImageUploadRequest(); 21 | 22 | request.SetFileStream(image, name, contentType); 23 | 24 | var response = await _client.MakeRequestAsync(request); 25 | 26 | if (response.StatusCode == HttpStatusCode.Created) 27 | return StreamJsonConverter.DeserializeObject(response.Content); 28 | 29 | throw StreamException.FromResponse(response); 30 | } 31 | 32 | public async Task DeleteAsync(string url) 33 | { 34 | var request = _client.BuildAppRequest("images/", HttpMethod.Delete); 35 | request.AddQueryParameter("url", url); 36 | 37 | var response = await _client.MakeRequestAsync(request); 38 | if (response.StatusCode != HttpStatusCode.OK) 39 | throw StreamException.FromResponse(response); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/IPersonalization.cs: -------------------------------------------------------------------------------- 1 | using Stream.Models; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Stream 6 | { 7 | /// Client to interact with personalization. 8 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/personalization_introduction/?language=csharp 9 | public interface IPersonalization 10 | { 11 | /// Removes data from the given resource, adding the given params to the request. 12 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/personalization_introduction/?language=csharp 13 | Task DeleteAsync(string endpoint, IDictionary data); 14 | 15 | /// Returns data from the given resource, adding the given params to the request. 16 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/personalization_introduction/?language=csharp 17 | Task> GetAsync(string endpoint, IDictionary data); 18 | 19 | /// Sends data to the given resource, adding the given params to the request. 20 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/personalization_introduction/?language=csharp 21 | Task> PostAsync(string endpoint, IDictionary data); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Rest/RestRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | 4 | namespace Stream.Rest 5 | { 6 | internal class RestRequest 7 | { 8 | internal RestRequest(string resource, HttpMethod method) 9 | { 10 | Method = method; 11 | Resource = resource; 12 | } 13 | 14 | internal Dictionary QueryParameters { get; private set; } = new Dictionary(); 15 | internal Dictionary Headers { get; private set; } = new Dictionary(); 16 | internal HttpMethod Method { get; private set; } 17 | internal string Resource { get; private set; } 18 | internal string JsonBody { get; private set; } 19 | internal System.IO.Stream FileStream { get; private set; } 20 | internal string FileStreamContentType { get; private set; } 21 | internal string FileStreamName { get; private set; } 22 | 23 | internal void AddHeader(string name, string value) => Headers[name] = value; 24 | 25 | internal void AddQueryParameter(string name, string value) => QueryParameters[name] = value; 26 | 27 | internal void SetJsonBody(string json) => JsonBody = json; 28 | 29 | internal void SetFileStream(System.IO.Stream stream, string name, string contentType) 30 | { 31 | FileStream = stream; 32 | FileStreamName = name; 33 | FileStreamContentType = contentType; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Files.cs: -------------------------------------------------------------------------------- 1 | using Stream.Models; 2 | using Stream.Utils; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace Stream 8 | { 9 | public class Files : IFiles 10 | { 11 | private readonly StreamClient _client; 12 | 13 | public Files(StreamClient client) 14 | { 15 | _client = client; 16 | } 17 | 18 | public async Task UploadAsync(System.IO.Stream file, string name) 19 | => await UploadAsync(file, name, null); 20 | 21 | public async Task UploadAsync(System.IO.Stream file, string name, string contentType) 22 | { 23 | var request = _client.BuildFileUploadRequest(); 24 | request.SetFileStream(file, name, contentType); 25 | 26 | var response = await _client.MakeRequestAsync(request); 27 | 28 | if (response.StatusCode == HttpStatusCode.Created) 29 | return StreamJsonConverter.DeserializeObject(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 | 3 | 4 | STREAM MARK 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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 | /// Updates a reaction. 61 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp 62 | Task UpdateAsync(string reactionId, IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null); 63 | } 64 | } -------------------------------------------------------------------------------- /src/Utils/ActivityIdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Stream.Utils 6 | { 7 | /// Utility class to generate a unique activity id. 8 | public static class ActivityIdGenerator 9 | { 10 | private static DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 11 | private static long EpochTicks = Epoch.Ticks; 12 | 13 | // Difference in 100-nanosecond intervals between 14 | // UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970) 15 | private static long UuidEpochDifference = 122192928000000000; 16 | 17 | /// Generates an Activity ID for the given foreign ID and epoch timestamp. 18 | public static Guid GenerateId(string foreignId, int epoch) 19 | { 20 | return GenerateId(foreignId, Epoch.AddSeconds(epoch)); 21 | } 22 | 23 | /// 24 | /// Generates an Activity ID for the given foreign ID and timestamp. 25 | /// 26 | public static Guid GenerateId(string foreignId, DateTime timestamp) 27 | { 28 | var truncatedDate = new DateTime(timestamp.Year, timestamp.Month, timestamp.Day, timestamp.Hour, timestamp.Minute, timestamp.Second, timestamp.Millisecond, DateTimeKind.Utc); 29 | 30 | var unixNano = truncatedDate.Ticks - EpochTicks; 31 | var t = (ulong)(UuidEpochDifference + unixNano); 32 | 33 | long signedDigest; 34 | using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(foreignId))) 35 | { 36 | var hashAsInt = Murmur3.Hash(stream); 37 | 38 | if (hashAsInt > int.MaxValue) 39 | signedDigest = (long)hashAsInt - 4294967296; 40 | else 41 | signedDigest = hashAsInt; 42 | } 43 | 44 | long signMask; 45 | if (signedDigest > 0) 46 | { 47 | signMask = 0x100000000; 48 | } 49 | else 50 | { 51 | signedDigest *= -1; 52 | signMask = 0x000000000; 53 | } 54 | 55 | signedDigest = (signedDigest | signMask) | 0x800000000000; 56 | 57 | var nodeBytes = PutUint64((ulong)signedDigest); 58 | 59 | var finalBytes = new byte[16]; 60 | Array.Copy(PutUint32(TrimUlongToUint(t)), 0, finalBytes, 0, 4); 61 | Array.Copy(PutUint16(TrimUlongToUshort(t >> 32)), 0, finalBytes, 4, 2); 62 | Array.Copy(PutUint16((ushort)(0x1000 | TrimUlongToUshort(t >> 48))), 0, finalBytes, 6, 2); 63 | Array.Copy(PutUint16(TrimUlongToUshort(0x8080)), 0, finalBytes, 8, 2); 64 | 65 | // Now to the final 66 | Array.Copy(nodeBytes, 2, finalBytes, 10, 6); 67 | 68 | return new Guid(BytesToGuidString(finalBytes)); 69 | } 70 | 71 | private static byte[] PutUint64(ulong v) 72 | { 73 | var b = new byte[8]; 74 | b[0] = (byte)((v >> 56) & 0xFF); 75 | b[1] = (byte)((v >> 48) & 0xFF); 76 | b[2] = (byte)((v >> 40) & 0xFF); 77 | b[3] = (byte)((v >> 32) & 0xFF); 78 | b[4] = (byte)((v >> 24) & 0xFF); 79 | b[5] = (byte)((v >> 16) & 0xFF); 80 | b[6] = (byte)((v >> 8) & 0xFF); 81 | b[7] = (byte)(v & 0xFF); 82 | 83 | return b; 84 | } 85 | 86 | private static byte[] PutUint32(uint v) 87 | { 88 | var b = new byte[4]; 89 | b[0] = (byte)((v >> 24) & 0xFF); 90 | b[1] = (byte)((v >> 16) & 0xFF); 91 | b[2] = (byte)((v >> 8) & 0xFF); 92 | b[3] = (byte)(v & 0xFF); 93 | 94 | return b; 95 | } 96 | 97 | private static byte[] PutUint16(ushort v) 98 | { 99 | var b = new byte[2]; 100 | b[0] = (byte)((v >> 8) & 0xFF); 101 | b[1] = (byte)(v & 0xFF); 102 | 103 | return b; 104 | } 105 | 106 | private static uint TrimUlongToUint(ulong l) 107 | { 108 | return (uint)(l & 0xFFFFFFFF); 109 | } 110 | 111 | private static ushort TrimUlongToUshort(ulong l) 112 | { 113 | return (ushort)(l & 0xFFFF); 114 | } 115 | 116 | private static string BytesToGuidString(byte[] b) 117 | { 118 | var offsets = new int[16] { 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 }; 119 | var hexString = "0123456789abcdef"; 120 | var retVal = new byte[36]; 121 | for (var i = 0; i < b.Length; i++) 122 | { 123 | var value = b[i]; 124 | retVal[offsets[i]] = (byte)hexString[value >> 4]; 125 | retVal[offsets[i] + 1] = (byte)hexString[value & 0xF]; 126 | } 127 | 128 | const int dash = 45; // The dash character '-' 129 | retVal[8] = retVal[13] = retVal[18] = retVal[23] = dash; 130 | 131 | return Encoding.UTF8.GetString(retVal); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/ClientTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Stream; 3 | using Stream.Models; 4 | using Stream.Utils; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | 9 | namespace StreamNetTests 10 | { 11 | [TestFixture] 12 | public class ClientTests : TestBase 13 | { 14 | [Test] 15 | public void TestClientArgumentsValidation() 16 | { 17 | Assert.Throws(() => 18 | { 19 | var client = new StreamClient(string.Empty, "asfd"); 20 | }); 21 | Assert.Throws(() => 22 | { 23 | var client = new StreamClient("asdf", null); 24 | }); 25 | } 26 | 27 | [Test] 28 | public void TestFeedIdValidation() 29 | { 30 | Assert.Throws(() => 31 | { 32 | var feed = Client.Feed(null, null); 33 | }); 34 | Assert.Throws(() => 35 | { 36 | var feed = Client.Feed("flat", string.Empty); 37 | }); 38 | Assert.Throws(() => 39 | { 40 | var feed = Client.Feed(string.Empty, "1"); 41 | }); 42 | Assert.Throws(() => 43 | { 44 | var feed = Client.Feed("flat:1", "2"); 45 | }); 46 | Assert.Throws(() => 47 | { 48 | var feed = Client.Feed("flat 1", "2"); 49 | }); 50 | Assert.Throws(() => 51 | { 52 | var feed = Client.Feed("flat1", "2:3"); 53 | }); 54 | } 55 | 56 | [Test] 57 | public void TestFollowFeedIdValidation() 58 | { 59 | var user1 = Client.Feed("user", "11"); 60 | 61 | Assert.ThrowsAsync(async () => 62 | { 63 | await user1.FollowFeedAsync(null, null); 64 | }); 65 | Assert.ThrowsAsync(async () => 66 | { 67 | await user1.FollowFeedAsync("flat", string.Empty); 68 | }); 69 | Assert.ThrowsAsync(async () => 70 | { 71 | await user1.FollowFeedAsync(string.Empty, "1"); 72 | }); 73 | Assert.ThrowsAsync(async () => 74 | { 75 | await user1.FollowFeedAsync("flat:1", "2"); 76 | }); 77 | Assert.ThrowsAsync(async () => 78 | { 79 | await user1.FollowFeedAsync("flat 1", "2"); 80 | }); 81 | Assert.ThrowsAsync(async () => 82 | { 83 | await user1.FollowFeedAsync("flat1", "2:3"); 84 | }); 85 | } 86 | 87 | [Test] 88 | public void TestActivityPartialUpdateArgumentValidation() 89 | { 90 | Assert.ThrowsAsync(async () => 91 | { 92 | await Client.ActivityPartialUpdateAsync(); 93 | }); 94 | Assert.ThrowsAsync(async () => 95 | { 96 | await Client.ActivityPartialUpdateAsync("id", new ForeignIdTime("fid", DateTime.Now)); 97 | }); 98 | } 99 | 100 | [Test] 101 | public void TestToken() 102 | { 103 | var result = DecodeJwt(Client.CreateUserToken("user")); 104 | Assert.AreEqual("user", (string)result["user_id"]); 105 | 106 | var extra = new Dictionary() 107 | { 108 | { "client", "dotnet" }, 109 | { "testing", true }, 110 | }; 111 | result = DecodeJwt(Client.CreateUserToken("user2", extra)); 112 | 113 | Assert.AreEqual("user2", (string)result["user_id"]); 114 | Assert.AreEqual("dotnet", (string)result["client"]); 115 | Assert.AreEqual(true, (bool)result["testing"]); 116 | Assert.False(result.ContainsKey("missing")); 117 | } 118 | 119 | [Test] 120 | public void TestModerationToken() 121 | { 122 | var result = DecodeJwt(Client.CreateUserToken("user")); 123 | Assert.AreEqual("user", (string)result["user_id"]); 124 | 125 | var extra = new Dictionary() 126 | { 127 | { "client", "dotnet" }, 128 | { "required_moderation_template", "mod_template_1" }, 129 | }; 130 | result = DecodeJwt(Client.CreateUserToken("user2", extra)); 131 | 132 | Assert.AreEqual("mod_template_1", (string)result["required_moderation_template"]); 133 | Assert.False(result.ContainsKey("missing")); 134 | } 135 | 136 | private Dictionary DecodeJwt(string token) 137 | { 138 | var segment = token.Split('.')[1]; 139 | var mod = segment.Length % 4; 140 | 141 | if (mod > 0) 142 | { 143 | segment += string.Empty.PadLeft(4 - mod, '='); 144 | } 145 | 146 | var encoded = Convert.FromBase64String(segment.Replace('-', '+').Replace('_', '/')); 147 | var payload = Encoding.UTF8.GetString(encoded); 148 | return StreamJsonConverter.DeserializeObject>(payload); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Models/GetOptions.cs: -------------------------------------------------------------------------------- 1 | using Stream.Rest; 2 | using Stream.Utils; 3 | using System.Collections.Generic; 4 | 5 | namespace Stream.Models 6 | { 7 | public class GetOptions 8 | { 9 | private const int DefaultOffset = 0; 10 | private const int DefaultLimit = 20; 11 | private int _offset = DefaultOffset; 12 | private int _limit = DefaultLimit; 13 | private FeedFilter _filter; 14 | private ActivityMarker _marker = null; 15 | private ReactionOption _reaction = null; 16 | private string _ranking = null; 17 | private string _session = null; 18 | private string _endpoint = null; 19 | private string _feed_slug = null; 20 | private string _user_id = null; 21 | private string _ranking_vars = null; 22 | private bool _score_vars = false; 23 | private string _discard_actors = null; 24 | private string _discard_actors_sep = null; 25 | private string _moderation_template = null; 26 | 27 | private IDictionary _custom = null; 28 | 29 | public GetOptions WithOffset(int offset) 30 | { 31 | _offset = offset; 32 | return this; 33 | } 34 | 35 | public GetOptions WithLimit(int limit) 36 | { 37 | _limit = limit; 38 | return this; 39 | } 40 | 41 | public GetOptions WithFilter(FeedFilter filter) 42 | { 43 | _filter = filter; 44 | return this; 45 | } 46 | 47 | public GetOptions WithMarker(ActivityMarker marker) 48 | { 49 | _marker = marker; 50 | return this; 51 | } 52 | 53 | public GetOptions WithReaction(ReactionOption reactions) 54 | { 55 | _reaction = reactions; 56 | return this; 57 | } 58 | 59 | public GetOptions WithRanking(string rankingSlug) 60 | { 61 | _ranking = rankingSlug; 62 | return this; 63 | } 64 | 65 | public GetOptions WithScoreVars() 66 | { 67 | _score_vars = true; 68 | return this; 69 | } 70 | 71 | public GetOptions WithRankingVars(IDictionary rankingVars) 72 | { 73 | _ranking_vars = StreamJsonConverter.SerializeObject(rankingVars); 74 | return this; 75 | } 76 | 77 | public GetOptions WithSession(string session) 78 | { 79 | _session = session; 80 | return this; 81 | } 82 | 83 | public GetOptions WithEndpoint(string endpoint) 84 | { 85 | _endpoint = endpoint; 86 | return this; 87 | } 88 | 89 | public GetOptions WithFeedSlug(string feedSlug) 90 | { 91 | _feed_slug = feedSlug; 92 | return this; 93 | } 94 | 95 | public GetOptions WithUserId(string userId) 96 | { 97 | _user_id = userId; 98 | return this; 99 | } 100 | 101 | public GetOptions WithCustom(string key, string value) 102 | { 103 | if (_custom == null) 104 | { 105 | _custom = new Dictionary(); 106 | } 107 | 108 | _custom.Add(key, value); 109 | return this; 110 | } 111 | 112 | public GetOptions DiscardActors(List actors, string separator = ",") 113 | { 114 | if (separator != ",") 115 | { 116 | _discard_actors_sep = separator; 117 | } 118 | 119 | _discard_actors = string.Join(separator, actors); 120 | return this; 121 | } 122 | 123 | internal void Apply(RestRequest request) 124 | { 125 | request.AddQueryParameter("offset", _offset.ToString()); 126 | request.AddQueryParameter("limit", _limit.ToString()); 127 | 128 | if (!string.IsNullOrWhiteSpace(_ranking)) 129 | request.AddQueryParameter("ranking", _ranking); 130 | 131 | if (!string.IsNullOrWhiteSpace(_session)) 132 | request.AddQueryParameter("session", _session); 133 | 134 | if (!string.IsNullOrWhiteSpace(_endpoint)) 135 | request.AddQueryParameter("endpoint", _endpoint); 136 | 137 | if (!string.IsNullOrWhiteSpace(_feed_slug)) 138 | request.AddQueryParameter("feed_slug", _feed_slug); 139 | 140 | if (!string.IsNullOrWhiteSpace(_user_id)) 141 | request.AddQueryParameter("user_id", _user_id); 142 | 143 | if (!string.IsNullOrWhiteSpace(_ranking_vars)) 144 | request.AddQueryParameter("ranking_vars", _ranking_vars); 145 | 146 | if (!string.IsNullOrWhiteSpace(_moderation_template)) 147 | request.AddQueryParameter("moderation_template", _moderation_template); 148 | 149 | if (_score_vars) 150 | request.AddQueryParameter("withScoreVars", "true"); 151 | 152 | if (!string.IsNullOrWhiteSpace(_discard_actors_sep)) 153 | request.AddQueryParameter("discard_actors_sep", _discard_actors_sep); 154 | 155 | if (!string.IsNullOrWhiteSpace(_discard_actors)) 156 | request.AddQueryParameter("discard_actors", _discard_actors); 157 | 158 | if (_custom != null) 159 | { 160 | foreach (KeyValuePair kvp in _custom) 161 | request.AddQueryParameter(kvp.Key, kvp.Value); 162 | } 163 | 164 | _filter?.Apply(request); 165 | _marker?.Apply(request); 166 | _reaction?.Apply(request); 167 | } 168 | 169 | public static GetOptions Default => new GetOptions(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/ActivityTests/NotificationActivityTests.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 NotificationActivityTests : TestBase 10 | { 11 | [Test] 12 | public async Task TestMarkRead() 13 | { 14 | var newActivity = new Activity("1", "tweet", "1"); 15 | var first = await NotificationFeed.AddActivityAsync(newActivity); 16 | 17 | newActivity = new Activity("1", "run", "2"); 18 | var second = await NotificationFeed.AddActivityAsync(newActivity); 19 | 20 | newActivity = new Activity("1", "share", "3"); 21 | var third = await NotificationFeed.AddActivityAsync(newActivity); 22 | 23 | var activities = (await NotificationFeed.GetNotificationActivities(GetOptions.Default.WithLimit(2).WithMarker(ActivityMarker.Mark().AllRead()))).Results; 24 | Assert.IsNotNull(activities); 25 | Assert.AreEqual(2, activities.Count()); 26 | 27 | var notActivity = activities.First(); 28 | Assert.IsNotNull(notActivity); 29 | Assert.IsFalse(notActivity.IsRead); 30 | 31 | notActivity = activities.Skip(1).First(); 32 | Assert.IsNotNull(notActivity); 33 | Assert.IsFalse(notActivity.IsRead); 34 | 35 | activities = (await NotificationFeed.GetNotificationActivities(GetOptions.Default.WithLimit(2).WithMarker(ActivityMarker.Mark().AllRead()))).Results; 36 | Assert.IsNotNull(activities); 37 | Assert.AreEqual(2, activities.Count()); 38 | 39 | notActivity = activities.First(); 40 | Assert.IsNotNull(notActivity); 41 | Assert.IsTrue(notActivity.IsRead); 42 | 43 | notActivity = activities.Skip(1).First(); 44 | Assert.IsNotNull(notActivity); 45 | Assert.IsTrue(notActivity.IsRead); 46 | } 47 | 48 | [Test] 49 | public async Task TestMarkReadByIds() 50 | { 51 | var newActivity = new Activity("1", "tweet", "1"); 52 | var first = await NotificationFeed.AddActivityAsync(newActivity); 53 | 54 | newActivity = new Activity("1", "run", "2"); 55 | var second = await NotificationFeed.AddActivityAsync(newActivity); 56 | 57 | newActivity = new Activity("1", "share", "3"); 58 | var third = await NotificationFeed.AddActivityAsync(newActivity); 59 | 60 | var activities = (await NotificationFeed.GetNotificationActivities(GetOptions.Default.WithLimit(2))).Results; 61 | 62 | var marker = ActivityMarker.Mark(); 63 | foreach (var activity in activities) 64 | { 65 | marker = marker.Read(activity.Id); 66 | } 67 | 68 | var notActivity = activities.First(); 69 | Assert.IsNotNull(notActivity); 70 | Assert.IsFalse(notActivity.IsRead); 71 | 72 | notActivity = activities.Skip(1).First(); 73 | Assert.IsNotNull(notActivity); 74 | Assert.IsFalse(notActivity.IsRead); 75 | 76 | activities = (await NotificationFeed.GetNotificationActivities(GetOptions.Default.WithLimit(3).WithMarker(marker))).Results; 77 | 78 | activities = (await NotificationFeed.GetNotificationActivities(GetOptions.Default.WithLimit(3))).Results; 79 | Assert.IsNotNull(activities); 80 | Assert.AreEqual(3, activities.Count()); 81 | 82 | notActivity = activities.First(); 83 | Assert.IsNotNull(notActivity); 84 | Assert.IsTrue(notActivity.IsRead); 85 | 86 | notActivity = activities.Skip(1).First(); 87 | Assert.IsNotNull(notActivity); 88 | Assert.IsTrue(notActivity.IsRead); 89 | 90 | notActivity = activities.Skip(2).First(); 91 | Assert.IsNotNull(notActivity); 92 | Assert.IsFalse(notActivity.IsRead); 93 | } 94 | 95 | [Test] 96 | public async Task TestMarkNotificationsRead() 97 | { 98 | var newActivity = new Activity("1", "tweet", "1"); 99 | var first = await NotificationFeed.AddActivityAsync(newActivity); 100 | 101 | newActivity = new Activity("1", "run", "2"); 102 | var second = await NotificationFeed.AddActivityAsync(newActivity); 103 | 104 | newActivity = new Activity("1", "share", "3"); 105 | var third = await NotificationFeed.AddActivityAsync(newActivity); 106 | 107 | var response = await NotificationFeed.GetNotificationActivities(GetOptions.Default.WithLimit(2).WithMarker(ActivityMarker.Mark().AllRead())); 108 | Assert.IsNotNull(response); 109 | 110 | var activities = response.Results; 111 | Assert.IsNotNull(activities); 112 | Assert.AreEqual(2, activities.Count()); 113 | 114 | var notActivity = activities.First(); 115 | Assert.IsNotNull(notActivity); 116 | Assert.IsFalse(notActivity.IsRead); 117 | 118 | notActivity = activities.Skip(1).First(); 119 | Assert.IsNotNull(notActivity); 120 | Assert.IsFalse(notActivity.IsRead); 121 | 122 | response = await NotificationFeed.GetNotificationActivities(GetOptions.Default.WithLimit(2)); 123 | 124 | Assert.IsNotNull(response); 125 | Assert.AreEqual(0, response.Unread); 126 | 127 | activities = response.Results; 128 | Assert.IsNotNull(activities); 129 | Assert.AreEqual(2, activities.Count()); 130 | 131 | notActivity = activities.First(); 132 | Assert.IsNotNull(notActivity); 133 | Assert.IsTrue(notActivity.IsRead); 134 | 135 | notActivity = activities.Skip(1).First(); 136 | Assert.IsNotNull(notActivity); 137 | Assert.IsTrue(notActivity.IsRead); 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /src/Collections.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 Collections : ICollections 12 | { 13 | private readonly StreamClient _client; 14 | 15 | internal Collections(StreamClient client) 16 | { 17 | _client = client; 18 | } 19 | 20 | public async Task UpsertAsync(string collectionName, CollectionObject data) 21 | { 22 | return await UpsertManyAsync(collectionName, new[] { data }); 23 | } 24 | 25 | public async Task UpsertManyAsync(string collectionName, IEnumerable data) 26 | { 27 | var body = new Dictionary 28 | { 29 | { 30 | "data", new Dictionary> { { collectionName, data.Select(x => x.Flatten()) } } 31 | }, 32 | }; 33 | var request = _client.BuildAppRequest("collections/", HttpMethod.Post); 34 | request.SetJsonBody(StreamJsonConverter.SerializeObject(body)); 35 | 36 | var response = await _client.MakeRequestAsync(request); 37 | 38 | if (response.StatusCode == HttpStatusCode.Created) 39 | return StreamJsonConverter.DeserializeObject(response.Content); 40 | 41 | throw StreamException.FromResponse(response); 42 | } 43 | 44 | public async Task SelectAsync(string collectionName, string id) 45 | { 46 | var result = await SelectManyAsync(collectionName, new[] { id }); 47 | return result.Response.Data.FirstOrDefault(); 48 | } 49 | 50 | public async Task SelectManyAsync(string collectionName, IEnumerable ids) 51 | { 52 | var request = _client.BuildAppRequest("collections/", HttpMethod.Get); 53 | request.AddQueryParameter("foreign_ids", string.Join(",", ids.Select(x => $"{collectionName}:{x}"))); 54 | 55 | var response = await _client.MakeRequestAsync(request); 56 | 57 | if (response.StatusCode == HttpStatusCode.OK) 58 | return StreamJsonConverter.DeserializeObject(response.Content); 59 | 60 | throw StreamException.FromResponse(response); 61 | } 62 | 63 | public async Task DeleteManyAsync(string collectionName, IEnumerable ids) 64 | { 65 | var request = _client.BuildAppRequest("collections/", HttpMethod.Delete); 66 | request.AddQueryParameter("collection_name", collectionName); 67 | request.AddQueryParameter("ids", string.Join(",", ids)); 68 | 69 | var response = await _client.MakeRequestAsync(request); 70 | 71 | if (response.StatusCode != HttpStatusCode.OK) 72 | throw StreamException.FromResponse(response); 73 | } 74 | 75 | public async Task AddAsync(string collectionName, Dictionary data, string id = null, string userId = null) 76 | { 77 | var collectionObject = new CollectionObject(id) { UserId = userId }; 78 | data.ForEach(x => collectionObject.SetData(x.Key, x.Value)); 79 | 80 | var request = _client.BuildAppRequest($"collections/{collectionName}/", HttpMethod.Post); 81 | request.SetJsonBody(StreamJsonConverter.SerializeObject(collectionObject)); 82 | 83 | var response = await _client.MakeRequestAsync(request); 84 | 85 | if (response.StatusCode != HttpStatusCode.Created) 86 | throw StreamException.FromResponse(response); 87 | 88 | return StreamJsonConverter.DeserializeObject(response.Content); 89 | } 90 | 91 | public async Task GetAsync(string collectionName, string id) 92 | { 93 | var request = _client.BuildAppRequest($"collections/{collectionName}/{id}/", HttpMethod.Get); 94 | 95 | var response = await _client.MakeRequestAsync(request); 96 | 97 | if (response.StatusCode != HttpStatusCode.OK) 98 | throw StreamException.FromResponse(response); 99 | 100 | return StreamJsonConverter.DeserializeObject(response.Content); 101 | } 102 | 103 | public async Task UpdateAsync(string collectionName, string id, Dictionary data) 104 | { 105 | var request = _client.BuildAppRequest($"collections/{collectionName}/{id}/", HttpMethod.Put); 106 | request.SetJsonBody(StreamJsonConverter.SerializeObject(new { data = data })); 107 | 108 | var response = await _client.MakeRequestAsync(request); 109 | 110 | if (response.StatusCode != HttpStatusCode.Created) 111 | throw StreamException.FromResponse(response); 112 | 113 | return StreamJsonConverter.DeserializeObject(response.Content); 114 | } 115 | 116 | public async Task DeleteAsync(string collectionName, string id) 117 | { 118 | var request = _client.BuildAppRequest($"collections/{collectionName}/{id}/", HttpMethod.Delete); 119 | 120 | var response = await _client.MakeRequestAsync(request); 121 | 122 | if (response.StatusCode == HttpStatusCode.OK) 123 | return StreamJsonConverter.DeserializeObject(response.Content); 124 | 125 | throw StreamException.FromResponse(response); 126 | } 127 | 128 | public string Ref(string collectionName, string collectionObjectId) => Ref(collectionName, new CollectionObject(collectionObjectId)); 129 | public string Ref(string collectionName, CollectionObject obj) => obj.Ref(collectionName); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/ModerationTests.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 | using Stream.Models; 10 | 11 | [TestFixture] 12 | public class ModerationTests : TestBase 13 | { 14 | [Test] 15 | public async Task TestModerationTemplate() 16 | { 17 | var newActivity2 = new Activity("1", "test", "2") 18 | { 19 | ForeignId = "r-test-2", 20 | Time = DateTime.Parse("2000-08-17T16:32:32"), 21 | }; 22 | newActivity2.SetData("moderation_template", "moderation_template_test_images"); 23 | 24 | newActivity2.SetData("text", "pissoar"); 25 | 26 | var attachments = new Dictionary(); 27 | string[] images = new string[] { "image1", "image2" }; 28 | attachments["images"] = images; 29 | 30 | newActivity2.SetData("attachment", attachments); 31 | 32 | var response = await this.UserFeed.AddActivityAsync(newActivity2); 33 | 34 | var modResponse = response.GetData("moderation"); 35 | 36 | Assert.AreEqual(modResponse.Status, "complete"); 37 | Assert.AreEqual(modResponse.RecommendedAction, "remove"); 38 | } 39 | 40 | [Test] 41 | public async Task TestReactionModeration() 42 | { 43 | var a = new Activity("user:1", "like", "cake") 44 | { 45 | ForeignId = "cake:1", 46 | Time = DateTime.UtcNow, 47 | Target = "johnny", 48 | }; 49 | 50 | var activity = await this.UserFeed.AddActivityAsync(a); 51 | 52 | var data = new Dictionary() { { "field", "value" }, { "number", 2 }, { "text", "pissoar" }, }; 53 | 54 | var r = await Client.Reactions.AddAsync("like", activity.Id, "bobby", data, null, "moderation_config_1_reaction"); 55 | 56 | Assert.NotNull(r); 57 | Assert.AreEqual(r.ActivityId, activity.Id); 58 | Assert.AreEqual(r.Kind, "like"); 59 | Assert.AreEqual(r.UserId, "bobby"); 60 | Assert.AreEqual(r.Data, data); 61 | 62 | var response = r.GetModerationResponse(); 63 | 64 | Assert.AreEqual("complete", response.Status); 65 | Assert.AreEqual("remove", response.RecommendedAction); 66 | 67 | var updatedData = new Dictionary() { { "field", "updated" }, { "number", 3 }, { "text", "pissoar" }, }; 68 | var updatedReaction = await Client.Reactions.UpdateAsync(r.Id, updatedData, null, "moderation_config_1_reaction"); 69 | 70 | Assert.NotNull(updatedReaction); 71 | Assert.AreEqual(updatedReaction.Id, r.Id); 72 | Assert.AreEqual(updatedReaction.Data["field"], "updated"); 73 | Assert.AreEqual(updatedReaction.Data["number"], 3); 74 | 75 | var updatedResponse = updatedReaction.GetModerationResponse(); 76 | Assert.AreEqual("complete", updatedResponse.Status); 77 | Assert.AreEqual("remove", updatedResponse.RecommendedAction); 78 | 79 | var updatedData2 = new Dictionary() { { "field", "updated" }, { "number", 3 }, { "text", "hello" }, }; 80 | var updatedReaction2 = await Client.Reactions.UpdateAsync(r.Id, updatedData2, null, "moderation_config_1_reaction"); 81 | 82 | Assert.NotNull(updatedReaction2); 83 | Assert.AreEqual(updatedReaction2.Id, r.Id); 84 | Assert.AreEqual(updatedReaction2.Data["field"], "updated"); 85 | Assert.AreEqual(updatedReaction2.Data["number"], 3); 86 | 87 | var updatedResponse2 = updatedReaction2.GetModerationResponse(); 88 | Assert.AreEqual("complete", updatedResponse2.Status); 89 | Assert.AreEqual("remove", updatedResponse2.RecommendedAction); 90 | 91 | var c1 = await Client.Reactions.AddChildAsync(r.Id, "upvote", "tommy", updatedData, null, "moderation_config_1_reaction"); 92 | Assert.NotNull(c1); 93 | var updatedResponse3 = c1.GetModerationResponse(); 94 | Assert.AreEqual("complete", updatedResponse3.Status); 95 | Assert.AreEqual("remove", updatedResponse3.RecommendedAction); 96 | } 97 | 98 | [Test] 99 | public async Task TestFlagUser() 100 | { 101 | var userId = "flagginguser"; 102 | var userData = new Dictionary 103 | { 104 | { "field", "value" }, 105 | { "is_admin", true }, 106 | }; 107 | 108 | var u = await Client.Users.AddAsync(userId, userData, true); 109 | 110 | Assert.NotNull(u); 111 | Assert.NotNull(u.CreatedAt); 112 | Assert.NotNull(u.UpdatedAt); 113 | 114 | var response = await Client.Moderation.FlagUserAsync(userId, "flagged-user", "blood"); 115 | Assert.NotNull(response); 116 | } 117 | 118 | [Test] 119 | public async Task TestFlagUserError() 120 | { 121 | Assert.ThrowsAsync(async () => await Client.Moderation.FlagUserAsync("", string.Empty, "blood")); 122 | } 123 | 124 | [Test] 125 | public async Task TestFlagActivity() 126 | { 127 | var newActivity = new Activity("vishal", "test", "1"); 128 | newActivity.SetData("stringint", "42"); 129 | newActivity.SetData("stringdouble", "42.2"); 130 | newActivity.SetData("stringcomplex", "{ \"test1\": 1, \"test2\": \"testing\" }"); 131 | 132 | // Set moderation data with origin_feed 133 | var moderationData = new Dictionary 134 | { 135 | { "origin_feed", this.UserFeed.FeedId } 136 | }; 137 | newActivity.SetData("moderation", moderationData); 138 | 139 | var response = await this.UserFeed.AddActivityAsync(newActivity); 140 | Assert.IsNotNull(response); 141 | 142 | var response1 = await Client.Moderation.FlagActivityAsync(response.Id, response.Actor, "blood"); 143 | 144 | Assert.NotNull(response1); 145 | } 146 | 147 | [Test] 148 | public async Task TestFlagReaction() 149 | { 150 | var a = new Activity("user:1", "like", "cake") 151 | { 152 | ForeignId = "cake:1", 153 | Time = DateTime.UtcNow, 154 | Target = "johnny", 155 | }; 156 | var activity = await this.UserFeed.AddActivityAsync(a); 157 | var data = new Dictionary() { { "field", "value" }, { "number", 2 }, }; 158 | var r = await Client.Reactions.AddAsync("like", activity.Id, "bobby", data); 159 | Assert.NotNull(r); 160 | 161 | var response = await Client.Moderation.FlagReactionAsync(r.Id, r.UserId, "blood"); 162 | Assert.NotNull(response); 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /src/IStreamFeed.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 a specific feed. 9 | /// A Feed is like a Stack (FILO) of activities. Activities can be pushed directly to a Feed. They can 10 | /// also be propagated from feeds that they follow (see: “Follow Relationships” and “Fan-out”). 11 | /// A single application may have multiple feeds. For example, you might have a user's feed (what they posted), 12 | /// their timeline feed (what the people they follow posted), and a notification feed (to alert them of 13 | /// engagement with activities they posted). 14 | /// 15 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 16 | public interface IStreamFeed 17 | { 18 | /// The feed id is the unique identifier of the feed whichs consists of the feed slug and the user id. 19 | string FeedId { get; } 20 | 21 | /// The start of the relative url of the feed. 22 | string UrlPath { get; } 23 | 24 | /// The start of the relative url of the feed for enrichment. 25 | string EnrichedPath { get; } 26 | 27 | /// The start of the relative url of the feed for enrichment. 28 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 29 | Task AddActivitiesAsync(IEnumerable activities); 30 | 31 | Task BatchUpdateActivityToTargetsAsync(List reqs); 32 | 33 | /// Add a new activity to the feed. 34 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 35 | Task AddActivityAsync(Activity activity); 36 | 37 | /// Returns a paginated list of the feed's followers. 38 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 39 | Task> FollowersAsync(int offset = 0, int limit = 25, IEnumerable filterBy = null); 40 | 41 | /// Starts to follow another feed. 42 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 43 | Task FollowFeedAsync(IStreamFeed feedToFollow, int activityCopyLimit = 100); 44 | 45 | /// Starts to follow another feed. 46 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 47 | Task FollowFeedAsync(string targetFeedSlug, string targetUserId, int activityCopyLimit = 100); 48 | 49 | /// Returns the followings of the feed. 50 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 51 | Task> FollowingAsync(int offset = 0, int limit = 25, IEnumerable filterBy = null); 52 | 53 | /// Returns activities of the feed. 54 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 55 | Task> GetActivitiesAsync(int offset = 0, int limit = 20, FeedFilter filter = null, ActivityMarker marker = null); 56 | 57 | /// Returns flat activities of the feed. 58 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 59 | Task> GetFlatActivitiesAsync(GetOptions options = null); 60 | 61 | /// Returns aggregate activities of the feed. 62 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 63 | Task> GetAggregateActivitiesAsync(GetOptions options = null); 64 | 65 | /// Returns notification activities of the feed. 66 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 67 | Task GetNotificationActivities(GetOptions options = null); 68 | 69 | /// Returns enriched flat activities of the feed. 70 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 71 | Task> GetEnrichedFlatActivitiesAsync(GetOptions options = null); 72 | 73 | /// Returns enriched aggregate activities of the feed. 74 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 75 | Task> GetEnrichedAggregatedActivitiesAsync(GetOptions options = null); 76 | 77 | /// Returns enriched notification activities of the feed. 78 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 79 | Task> GetEnrichedNotificationActivitiesAsync(GetOptions options = null); 80 | 81 | /// Removes an activity from the feed. 82 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 83 | Task RemoveActivityAsync(string activityId, bool foreignId = false); 84 | 85 | /// Unfollows a feed. 86 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 87 | Task UnfollowFeedAsync(IStreamFeed feedToUnfollow, bool keepHistory = false); 88 | 89 | /// Unfollows a feed. 90 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 91 | Task UnfollowFeedAsync(string targetFeedSlug, string targetUserId, bool keepHistory = false); 92 | 93 | /// Updates activities of the feed. 94 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 95 | Task UpdateActivitiesAsync(IEnumerable activities); 96 | 97 | /// Updates an activity of the feed. 98 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 99 | Task UpdateActivityAsync(Activity activity); 100 | 101 | /// Returns following and follower stats of the feed. 102 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 103 | Task FollowStatsAsync(IEnumerable followersSlugs = null, IEnumerable followingSlugs = null); 104 | 105 | /// 106 | /// Updates the "to" targets for the provided activity, with the options passed 107 | /// as argument for replacing, adding, or removing to targets. 108 | /// 109 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 110 | Task UpdateActivityToTargetsAsync(ForeignIdTime foreignIdTime, 111 | IEnumerable adds = null, 112 | IEnumerable newTargets = null, 113 | IEnumerable removed = null); 114 | 115 | /// 116 | /// Updates the "to" targets for the provided activity, with the options passed 117 | /// as argument for replacing, adding, or removing to targets. 118 | /// 119 | /// https://getstream.io/activity-feeds/docs/dotnet-csharp/feeds_101/?language=csharp 120 | Task UpdateActivityToTargetsAsync(string id, 121 | IEnumerable adds = null, 122 | IEnumerable newTargets = null, 123 | IEnumerable removed = null); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Reactions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using Stream.Models; 3 | using Stream.Rest; 4 | using Stream.Utils; 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 Reactions : IReactions 13 | { 14 | private readonly StreamClient _client; 15 | 16 | internal Reactions(StreamClient client) 17 | { 18 | _client = client; 19 | } 20 | 21 | public async Task AddAsync(string kind, string activityId, string userId, 22 | IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null) 23 | { 24 | return await AddAsync(null, kind, activityId, userId, data, targetFeeds, moderationTemplate); 25 | } 26 | 27 | public async Task AddAsync(string reactionId, string kind, string activityId, string userId, 28 | IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null) 29 | { 30 | var r = new Reaction 31 | { 32 | Id = reactionId, 33 | Kind = kind, 34 | ActivityId = activityId, 35 | UserId = userId, 36 | Data = data, 37 | TargetFeeds = targetFeeds, 38 | ModerationTemplate = moderationTemplate, 39 | }; 40 | 41 | return await AddAsync(r); 42 | } 43 | 44 | public async Task AddChildAsync(Reaction parent, string kind, string userId, 45 | IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null) 46 | { 47 | return await AddChildAsync(parent.Id, null, kind, userId, data, targetFeeds, moderationTemplate); 48 | } 49 | 50 | public async Task AddChildAsync(string parentId, string kind, string userId, 51 | IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null) 52 | { 53 | return await AddChildAsync(parentId, null, kind, userId, data, targetFeeds, moderationTemplate); 54 | } 55 | 56 | public async Task AddChildAsync(string parentId, string reactionId, string kind, string userId, 57 | IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null) 58 | { 59 | var r = new Reaction() 60 | { 61 | Id = reactionId, 62 | Kind = kind, 63 | UserId = userId, 64 | Data = data, 65 | ParentId = parentId, 66 | TargetFeeds = targetFeeds, 67 | ModerationTemplate = moderationTemplate, 68 | }; 69 | 70 | return await AddAsync(r); 71 | } 72 | 73 | public async Task AddChildAsync(Reaction parent, string reactionId, string kind, string userId, 74 | IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null) 75 | { 76 | return await AddChildAsync(parent.Id, reactionId, kind, userId, data, targetFeeds, moderationTemplate); 77 | } 78 | 79 | public async Task GetAsync(string reactionId) 80 | { 81 | var request = _client.BuildAppRequest($"reaction/{reactionId}/", HttpMethod.Get); 82 | 83 | var response = await _client.MakeRequestAsync(request); 84 | 85 | if (response.StatusCode == HttpStatusCode.OK) 86 | return StreamJsonConverter.DeserializeObject(response.Content); 87 | 88 | throw StreamException.FromResponse(response); 89 | } 90 | 91 | public async Task> FilterAsync(ReactionFiltering filtering, ReactionPagination pagination) 92 | { 93 | var response = await FilterInternalAsync(filtering, pagination); 94 | 95 | if (response.StatusCode == HttpStatusCode.OK) 96 | { 97 | return StreamJsonConverter.DeserializeObject(response.Content).Results; 98 | } 99 | 100 | throw StreamException.FromResponse(response); 101 | } 102 | 103 | public async Task FilterWithActivityAsync(ReactionFiltering filtering, ReactionPagination pagination) 104 | { 105 | var response = await FilterInternalAsync(filtering.WithActivityData(), pagination); 106 | 107 | if (response.StatusCode == HttpStatusCode.OK) 108 | { 109 | var token = JToken.Parse(response.Content); 110 | var reactions = token.ToObject().Results; 111 | var activity = token["activity"].ToObject(); 112 | 113 | return new ReactionsWithActivity 114 | { 115 | Results = reactions, 116 | Activity = activity, 117 | }; 118 | } 119 | 120 | throw StreamException.FromResponse(response); 121 | } 122 | 123 | private async Task FilterInternalAsync(ReactionFiltering filtering, ReactionPagination pagination) 124 | { 125 | var urlPath = pagination.GetPath(); 126 | var request = _client.BuildAppRequest($"reaction/{urlPath}", HttpMethod.Get); 127 | filtering.Apply(request); 128 | 129 | var response = await _client.MakeRequestAsync(request); 130 | 131 | return response; 132 | } 133 | 134 | public async Task UpdateAsync(string reactionId, IDictionary data = null, IEnumerable targetFeeds = null, string moderationTemplate = null) 135 | { 136 | var r = new Reaction 137 | { 138 | Id = reactionId, 139 | Data = data, 140 | TargetFeeds = targetFeeds, 141 | ModerationTemplate = moderationTemplate, 142 | }; 143 | 144 | var request = _client.BuildAppRequest($"reaction/{reactionId}/", HttpMethod.Put); 145 | request.SetJsonBody(StreamJsonConverter.SerializeObject(r)); 146 | 147 | var response = await _client.MakeRequestAsync(request); 148 | 149 | if (response.StatusCode == HttpStatusCode.Created) 150 | return StreamJsonConverter.DeserializeObject(response.Content); 151 | 152 | throw StreamException.FromResponse(response); 153 | } 154 | 155 | public async Task DeleteAsync(string reactionId, bool soft = false) 156 | { 157 | var path = $"reaction/{reactionId}/"; 158 | var request = _client.BuildAppRequest(path, HttpMethod.Delete); 159 | if (soft) 160 | { 161 | request.AddQueryParameter("soft", "true"); 162 | } 163 | 164 | var response = await _client.MakeRequestAsync(request); 165 | 166 | if (response.StatusCode != HttpStatusCode.OK) 167 | throw StreamException.FromResponse(response); 168 | } 169 | 170 | public async Task RestoreSoftDeletedAsync(string reactionId) 171 | { 172 | var request = _client.BuildAppRequest($"reaction/{reactionId}/restore/", HttpMethod.Put); 173 | var response = await _client.MakeRequestAsync(request); 174 | 175 | if (response.StatusCode != HttpStatusCode.Created && response.StatusCode != HttpStatusCode.OK) 176 | throw StreamException.FromResponse(response); 177 | } 178 | 179 | private async Task AddAsync(Reaction r) 180 | { 181 | var request = _client.BuildAppRequest("reaction/", HttpMethod.Post); 182 | request.SetJsonBody(StreamJsonConverter.SerializeObject(r)); 183 | 184 | var response = await _client.MakeRequestAsync(request); 185 | 186 | if (response.StatusCode == HttpStatusCode.Created) 187 | return StreamJsonConverter.DeserializeObject(response.Content); 188 | 189 | throw StreamException.FromResponse(response); 190 | } 191 | } 192 | } -------------------------------------------------------------------------------- /samples/ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Stream; 7 | using Stream.Models; 8 | 9 | namespace ConsoleApp 10 | { 11 | class Program 12 | { 13 | static async Task Main(string[] args) 14 | { 15 | // Create a client, find your API keys here https://getstream.io/dashboard/ 16 | var client = new StreamClient(Environment.GetEnvironmentVariable("STREAM_API_KEY"), Environment.GetEnvironmentVariable("STREAM_API_SECRET")); 17 | // From this IStreamClient instance you can access a variety of API endpoints, 18 | // and it is also a factory class for other classes, such as IBatch, ICollections 19 | // IReactions, IStreamFeed etc. Let's take a look at IStreamFeed now. 20 | 21 | // Reference a feed 22 | var userFeed1 = client.Feed("user", "1"); 23 | 24 | // Get 10 activities starting from the 5th (slow offset pagination) 25 | var results = await userFeed1.GetActivitiesAsync(5, 10); 26 | 27 | // Create a new activity 28 | var activity = new Activity("1", "like", "3") 29 | { 30 | ForeignId = "post:42" 31 | }; 32 | 33 | await userFeed1.AddActivityAsync(activity); 34 | 35 | // Create a complex activity 36 | activity = new Activity("1", "run", "1") 37 | { 38 | ForeignId = "run:1" 39 | }; 40 | var course = new Dictionary(); 41 | course["name"] = "Shevlin Park"; 42 | course["distance"] = 10; 43 | activity.SetData("course", course); 44 | await userFeed1.AddActivityAsync(activity); 45 | 46 | // Remove an activity by its id 47 | await userFeed1.RemoveActivityAsync("e561de8f-00f1-11e4-b400-0cc47a024be0"); 48 | 49 | // Remove activities by their foreign_id 50 | await userFeed1.RemoveActivityAsync("post:42", true); 51 | 52 | // Let user 1 start following user 42's flat feed 53 | await userFeed1.FollowFeedAsync("flat", "42"); 54 | 55 | // Let user 1 stop following user 42's flat feed 56 | await userFeed1.UnfollowFeedAsync("flat", "42"); 57 | 58 | // Retrieve first 10 followers of a feed 59 | await userFeed1.FollowersAsync(0, 10); 60 | 61 | // Retrieve 2 to 10 followers 62 | await userFeed1.FollowersAsync(2, 10); 63 | 64 | // Retrieve 10 feeds followed by $user_feed_1 65 | await userFeed1.FollowingAsync(0, 10); 66 | 67 | // Retrieve 10 feeds followed by $user_feed_1 starting from the 10th (2nd page) 68 | await userFeed1.FollowingAsync(10, 20); 69 | 70 | // Check if $user_feed_1 follows specific feeds 71 | await userFeed1.FollowingAsync(0, 2, new[] { "user:42", "user:43" }); 72 | 73 | // Follow stats 74 | // Count the number of users following this feed 75 | // Count the number of tags are followed by this feed 76 | var stats = await userFeed1.FollowStatsAsync(new[] { "user" }, new[] { "tags" }); 77 | Console.WriteLine(stats.Results.Followers.Count); 78 | Console.WriteLine(stats.Results.Following.Count); 79 | 80 | // Retrieve activities by their ids 81 | var ids = new[] { "e561de8f-00f1-11e4-b400-0cc47a024be0", "a34ndjsh-00f1-11e4-b400-0c9jdnbn0eb0" }; 82 | var activities = await client.Batch.GetActivitiesByIdAsync(ids); 83 | 84 | // Retrieve activities by their ForeignID/Time 85 | var foreignIDTimes = new[] { new ForeignIdTime("fid-1", DateTime.Parse("2000-08-19T16:32:32")), new ForeignIdTime("fid-2", DateTime.Parse("2000-08-21T16:32:32")) }; 86 | activities = await client.Batch.GetActivitiesByForeignIdAsync(foreignIDTimes); 87 | 88 | // Partially update an activity 89 | var set = new Dictionary(); 90 | set.Add("custom_field", "new value"); 91 | var unset = new[] { "field to remove" }; 92 | 93 | // By id 94 | await client.ActivityPartialUpdateAsync("e561de8f-00f1-11e4-b400-0cc47a024be0", null, set, unset); 95 | 96 | // By foreign id and time 97 | var fidTime = new ForeignIdTime("fid-1", DateTime.Parse("2000-08-19T16:32:32")); 98 | await client.ActivityPartialUpdateAsync(null, fidTime, set, unset); 99 | 100 | // Add a reaction to an activity 101 | var activityData = new Activity("bob", "cook", "burger") 102 | { 103 | ForeignId = "post:42" 104 | }; 105 | activity = await userFeed1.AddActivityAsync(activity); 106 | var r = await client.Reactions.AddAsync("comment", activity.Id, "john"); 107 | 108 | // Add a reaction to a reaction 109 | var child = await client.Reactions.AddChildAsync(r, "upvote", "john"); 110 | 111 | // Enrich feed results 112 | var userData = new Dictionary() 113 | { 114 | {"is_admin", true}, 115 | {"nickname","bobby"} 116 | }; 117 | var u = await client.Users.AddAsync("timmy", userData); 118 | var userRef = u.Ref(); 119 | var a = new Activity(userRef, "add", "post"); 120 | var plainActivity = await userFeed1.AddActivityAsync(a); 121 | // Here plainActivity.Actor is just a plain string containing the user ref 122 | var enriched = await userFeed1.GetEnrichedFlatActivitiesAsync(); 123 | var actor = enriched.Results.First(); 124 | var userID = actor.GetData("id"); 125 | var data = actor.GetData>("data"); //this is `userData` 126 | 127 | // Enrich feed results with reactions 128 | activityData = new Activity("bob", "cook", "burger") 129 | { 130 | ForeignId = "post:42" 131 | }; 132 | activity = await userFeed1.AddActivityAsync(activity); 133 | var com = await client.Reactions.AddAsync("comment", activity.Id, "john"); 134 | var like = await client.Reactions.AddAsync("like", activity.Id, "maria"); 135 | 136 | // Fetch reaction counts grouped by reaction kind 137 | enriched = await userFeed1.GetEnrichedFlatActivitiesAsync(GetOptions.Default.WithReaction(ReactionOption.With().Counts())); 138 | var enrichedActivity = enriched.Results.First(); 139 | Console.WriteLine(enrichedActivity.ReactionCounts["like"]); // 1 140 | Console.WriteLine(enrichedActivity.ReactionCounts["comment"]); // 1 141 | 142 | // Fetch reactions grouped by reaction kind 143 | enriched = await userFeed1.GetEnrichedFlatActivitiesAsync(GetOptions.Default.WithReaction(ReactionOption.With().Own())); 144 | enrichedActivity = enriched.Results.First(); 145 | Console.WriteLine(enrichedActivity.OwnReactions["like"]); // is the $like reaction 146 | Console.WriteLine(enrichedActivity.OwnReactions["comment"]); // is the $comment reaction 147 | 148 | // All reactions enrichment can be selected 149 | enriched = await userFeed1.GetEnrichedFlatActivitiesAsync(GetOptions.Default.WithReaction(ReactionOption.With().Counts().Own().Recent())); 150 | 151 | // Personalization 152 | var input = new Dictionary 153 | { 154 | {"feed_slug", "my_personalized_feed"}, 155 | {"user_id", "john"}, 156 | {"limit", 20}, 157 | {"ranking", "my_ranking"} 158 | }; 159 | var response = await client.Personalization.GetAsync("my_endpoint", input); 160 | 161 | // File & Image Upload 162 | using (var fs = File.OpenRead("../../tests/helloworld.txt")) 163 | { 164 | var fileUpload = await client.Files.UploadAsync(fs, "helloworld"); 165 | } 166 | using (var fs = File.OpenRead("../../tests/helloworld.jpg")) 167 | { 168 | var imageupload = await client.Images.UploadAsync(fs, "helloworld", "image/jpeg"); 169 | } 170 | 171 | // Use fileUpload.File and imageUpload.File afterwards 172 | // Open graph 173 | var og = await client.OgAsync("https://google.com"); 174 | } 175 | } 176 | 177 | } 178 | 179 | -------------------------------------------------------------------------------- /tests/ReactionTests.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using NUnit.Framework; 3 | using Stream; 4 | using Stream.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text.Json; 9 | using System.Threading.Tasks; 10 | namespace StreamNetTests 11 | { 12 | [TestFixture] 13 | public class ReactionTests : TestBase 14 | { 15 | [Test] 16 | public async Task TestReactions() 17 | { 18 | var a = new Activity("user:1", "like", "cake") 19 | { 20 | ForeignId = "cake:1", 21 | Time = DateTime.UtcNow, 22 | Target = "johnny", 23 | }; 24 | 25 | var activity = await this.UserFeed.AddActivityAsync(a); 26 | 27 | var data = new Dictionary() { { "field", "value" }, { "number", 2 }, }; 28 | 29 | var r = await Client.Reactions.AddAsync("like", activity.Id, "bobby", data); 30 | 31 | Assert.NotNull(r); 32 | Assert.AreEqual(r.ActivityId, activity.Id); 33 | Assert.AreEqual(r.Kind, "like"); 34 | Assert.AreEqual(r.UserId, "bobby"); 35 | Assert.AreEqual(r.Data, data); 36 | Assert.True(r.CreatedAt.HasValue); 37 | Assert.True(r.UpdatedAt.HasValue); 38 | Assert.IsNotEmpty(r.Id); 39 | 40 | // get reaction 41 | Reaction r2 = null; 42 | Assert.DoesNotThrowAsync(async () => r2 = await Client.Reactions.GetAsync(r.Id)); 43 | 44 | Assert.NotNull(r2); 45 | Assert.AreEqual(r2.ActivityId, r.ActivityId); 46 | Assert.AreEqual(r2.Kind, "like"); 47 | Assert.AreEqual(r2.UserId, "bobby"); 48 | Assert.AreEqual(r2.Data, r.Data); 49 | Assert.AreEqual(r2.Id, r.Id); 50 | 51 | // Update reaction 52 | data["number"] = 321; 53 | data["new"] = "field"; 54 | data.Remove("field"); 55 | 56 | var beforeTime = r.UpdatedAt.Value; 57 | Assert.DoesNotThrowAsync(async () => r2 = await Client.Reactions.UpdateAsync(r.Id, data)); 58 | Assert.NotNull(r2); 59 | Assert.False(r2.Data.ContainsKey("field")); 60 | Assert.True(r2.Data.TryGetValue("number", out object n)); 61 | Assert.AreEqual((long)n, 321); 62 | Assert.True(r2.Data.ContainsKey("new")); 63 | 64 | // Add children 65 | var c1 = await Client.Reactions.AddChildAsync(r, "upvote", "tommy"); 66 | var c2 = await Client.Reactions.AddChildAsync(r, "downvote", "timmy"); 67 | var c3 = await Client.Reactions.AddChildAsync(r.Id, "upvote", "jimmy"); 68 | 69 | var parent = await Client.Reactions.GetAsync(r.Id); 70 | 71 | Assert.AreEqual(parent.ChildrenCounters["upvote"], 2); 72 | Assert.AreEqual(parent.ChildrenCounters["downvote"], 1); 73 | 74 | Assert.IsTrue(parent.LatestChildren["upvote"].Select(x => x.Id).Contains(c1.Id)); 75 | Assert.IsTrue(parent.LatestChildren["upvote"].Select(x => x.Id).Contains(c3.Id)); 76 | Assert.IsTrue(parent.LatestChildren["downvote"].Select(x => x.Id).Contains(c2.Id)); 77 | 78 | // restore tests once there is support on server 79 | // Assert.DoesNotThrowAsync(async () => await Client.Reactions.DeleteAsync(r.Id, true)); 80 | // Assert.DoesNotThrowAsync(async () => await Client.Reactions.RestoreSoftDeletedAsync(r.Id)); 81 | Assert.DoesNotThrowAsync(async () => await Client.Reactions.DeleteAsync(r.Id)); 82 | 83 | Assert.ThrowsAsync(async () => await Client.Reactions.GetAsync(r.Id)); 84 | } 85 | 86 | [Test] 87 | public async Task TestReactionPagination() 88 | { 89 | var a = new Activity("user:1", "like", "cake") 90 | { 91 | ForeignId = "cake:1", 92 | Time = DateTime.UtcNow, 93 | Target = "johnny", 94 | }; 95 | 96 | var activity = await this.UserFeed.AddActivityAsync(a); 97 | 98 | a.Time = DateTime.UtcNow; 99 | a.ForeignId = "cake:123"; 100 | var activity2 = await this.UserFeed.AddActivityAsync(a); 101 | 102 | var data = new Dictionary() { { "field", "value" }, { "number", 2 }, }; 103 | 104 | var userId = Guid.NewGuid().ToString(); 105 | 106 | var r1 = await Client.Reactions.AddAsync("like", activity.Id, userId, data); 107 | var r2 = await Client.Reactions.AddAsync("comment", activity.Id, userId, data); 108 | var r3 = await Client.Reactions.AddAsync("like", activity.Id, "bob", data); 109 | 110 | var r4 = await Client.Reactions.AddChildAsync(r3, "upvote", "tom", data); 111 | var r5 = await Client.Reactions.AddChildAsync( 112 | r3.Id, 113 | Guid.NewGuid().ToString(), 114 | "upvote", 115 | "mary", 116 | data); 117 | 118 | // activity id 119 | var filter = ReactionFiltering.Default; 120 | var pagination = ReactionPagination.By.ActivityId(activity.Id).Kind("like"); 121 | 122 | var reactionsByActivity = await Client.Reactions.FilterAsync(filter, pagination); 123 | Assert.AreEqual(2, reactionsByActivity.Count()); 124 | 125 | var r = (List)reactionsByActivity; 126 | var actual = r.Find(x => x.Id == r1.Id); 127 | 128 | Assert.NotNull(actual); 129 | Assert.AreEqual(r1.Id, actual.Id); 130 | Assert.AreEqual(r1.Kind, actual.Kind); 131 | Assert.AreEqual(r1.ActivityId, actual.ActivityId); 132 | 133 | actual = r.Find(x => x.Id == r3.Id); 134 | 135 | Assert.NotNull(actual); 136 | Assert.AreEqual(r3.Id, actual.Id); 137 | Assert.AreEqual(r3.Kind, actual.Kind); 138 | Assert.AreEqual(r3.ActivityId, actual.ActivityId); 139 | 140 | // with limit 141 | reactionsByActivity = await Client.Reactions.FilterAsync( 142 | filter.WithLimit(1), 143 | pagination); 144 | Assert.AreEqual(1, reactionsByActivity.Count()); 145 | 146 | // with data 147 | var reactionsByActivityWithData = await Client.Reactions.FilterWithActivityAsync( 148 | filter.WithLimit(1), 149 | pagination); 150 | Assert.AreEqual(1, reactionsByActivity.Count()); 151 | Assert.AreEqual(data, reactionsByActivity.FirstOrDefault().Data); 152 | 153 | // user id 154 | filter = ReactionFiltering.Default; 155 | pagination = ReactionPagination.By.UserId(userId); 156 | 157 | var reactionsByUser = await Client.Reactions.FilterAsync(filter, pagination); 158 | Assert.AreEqual(2, reactionsByUser.Count()); 159 | 160 | r = (List)reactionsByUser; 161 | actual = r.Find(x => x.Id == r1.Id); 162 | 163 | Assert.NotNull(actual); 164 | Assert.AreEqual(r1.Id, actual.Id); 165 | Assert.AreEqual(r1.Kind, actual.Kind); 166 | Assert.AreEqual(r1.ActivityId, actual.ActivityId); 167 | 168 | actual = r.Find(x => x.Id == r2.Id); 169 | 170 | Assert.NotNull(actual); 171 | Assert.AreEqual(r2.Id, actual.Id); 172 | Assert.AreEqual(r2.Kind, actual.Kind); 173 | Assert.AreEqual(r2.ActivityId, actual.ActivityId); 174 | 175 | // reaction id 176 | filter = ReactionFiltering.Default; 177 | pagination = ReactionPagination.By.Kind("upvote").ReactionId(r3.Id); 178 | 179 | var reactionsByParent = await Client.Reactions.FilterAsync(filter, pagination); 180 | Assert.AreEqual(2, reactionsByParent.Count()); 181 | 182 | r = reactionsByParent.ToList(); 183 | actual = r.Find(x => x.Id == r4.Id); 184 | 185 | Assert.NotNull(actual); 186 | Assert.AreEqual(r4.Id, actual.Id); 187 | Assert.AreEqual(r4.Kind, actual.Kind); 188 | Assert.AreEqual(r4.ActivityId, actual.ActivityId); 189 | Assert.AreEqual(r4.UserId, actual.UserId); 190 | 191 | actual = r.Find(x => x.Id == r5.Id); 192 | 193 | Assert.NotNull(actual); 194 | Assert.AreEqual(r5.Id, actual.Id); 195 | Assert.AreEqual(r5.Kind, actual.Kind); 196 | Assert.AreEqual(r5.ActivityId, actual.ActivityId); 197 | Assert.AreEqual(r5.UserId, actual.UserId); 198 | } 199 | } 200 | } --------------------------------------------------------------------------------