diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 8c2b60b..02b87e1 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @ferhatelmas @peterdeme
+* @ferhatelmas
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 66e149d..6aff037 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -16,7 +16,7 @@ jobs:
env:
DOTNET_CLI_TELEMETRY_OPTOUT: "true"
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -28,7 +28,7 @@ jobs:
dotnet-version: 6.0.x
- name: Dependency cache
- uses: actions/cache@v2
+ uses: actions/cache@v3
id: cache
with:
path: ~/.nuget/packages
diff --git a/.github/workflows/initiate_release.yaml b/.github/workflows/initiate_release.yaml
index 2525e6e..88c7db7 100755
--- a/.github/workflows/initiate_release.yaml
+++ b/.github/workflows/initiate_release.yaml
@@ -11,7 +11,7 @@ jobs:
init_release:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 0 # gives the changelog generator access to all previous commits
@@ -19,7 +19,7 @@ jobs:
run: scripts/create_release_branch.sh "${{ github.event.inputs.version }}"
- name: Get changelog diff
- uses: actions/github-script@v5
+ uses: actions/github-script@v6
with:
script: |
const get_change_log_diff = require('./scripts/get_changelog_diff.js')
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index cb10856..aab6f3e 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -13,11 +13,11 @@ jobs:
env:
DOTNET_CLI_TELEMETRY_OPTOUT: "true"
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 0
- - uses: actions/github-script@v5
+ - uses: actions/github-script@v6
with:
script: |
const get_change_log_diff = require('./scripts/get_changelog_diff.js')
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml
index 2461d6f..7be0117 100644
--- a/.github/workflows/reviewdog.yml
+++ b/.github/workflows/reviewdog.yml
@@ -10,14 +10,14 @@ jobs:
reviewdog:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest
- name: Setup dotnet
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
diff --git a/src/IReactions.cs b/src/IReactions.cs
index f8abfd7..01d5668 100644
--- a/src/IReactions.cs
+++ b/src/IReactions.cs
@@ -13,10 +13,18 @@ namespace Stream
/// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
public interface IReactions
{
+ /// Posts a new reaciton.
+ /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
+ Task AddAsync(string reactionId, string kind, string activityId, string userId, IDictionary data = null, IEnumerable targetFeeds = null);
+
/// Posts a new reaciton.
/// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
Task AddAsync(string kind, string activityId, string userId, IDictionary data = null, IEnumerable targetFeeds = null);
+ /// Adds a new child reaction.
+ /// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
+ Task AddChildAsync(Reaction parent, string reactionId, string kind, string userId, IDictionary data = null, IEnumerable targetFeeds = null);
+
/// Adds a new child reaction.
/// https://getstream.io/activity-feeds/docs/dotnet-csharp/reactions_introduction/?language=csharp
Task AddChildAsync(Reaction parent, string kind, string userId, IDictionary data = null, IEnumerable targetFeeds = null);
diff --git a/src/Reactions.cs b/src/Reactions.cs
index 0c9602b..129c3bf 100644
--- a/src/Reactions.cs
+++ b/src/Reactions.cs
@@ -20,9 +20,16 @@ internal Reactions(StreamClient client)
public async Task AddAsync(string kind, string activityId, string userId,
IDictionary data = null, IEnumerable targetFeeds = null)
+ {
+ return await AddAsync(null, kind, activityId, userId, data, targetFeeds);
+ }
+
+ public async Task AddAsync(string reactionId, string kind, string activityId, string userId,
+ IDictionary data = null, IEnumerable targetFeeds = null)
{
var r = new Reaction
{
+ Id = reactionId,
Kind = kind,
ActivityId = activityId,
UserId = userId,
@@ -35,9 +42,16 @@ public async Task AddAsync(string kind, string activityId, string user
public async Task AddChildAsync(Reaction parent, string kind, string userId,
IDictionary data = null, IEnumerable targetFeeds = null)
+ {
+ return await AddChildAsync(parent, null, kind, userId, data, targetFeeds);
+ }
+
+ public async Task AddChildAsync(Reaction parent, string reactionId, string kind, string userId,
+ IDictionary data = null, IEnumerable targetFeeds = null)
{
var r = new Reaction()
{
+ Id = reactionId,
Kind = kind,
UserId = userId,
Data = data,
diff --git a/src/Rest/RestClient.cs b/src/Rest/RestClient.cs
index 5df8e99..ec9a853 100644
--- a/src/Rest/RestClient.cs
+++ b/src/Rest/RestClient.cs
@@ -66,16 +66,16 @@ private HttpContent CreateFileStream(RestRequest request)
private Uri BuildUri(RestRequest request)
{
var queryStringBuilder = new StringBuilder();
- request.QueryParameters.ForEach((p) =>
+ request.QueryParameters.ForEach(p =>
{
queryStringBuilder.Append(queryStringBuilder.Length == 0 ? "?" : "&");
- queryStringBuilder.Append($"{p.Key}={Uri.EscapeDataString(p.Value.ToString())}");
+ queryStringBuilder.Append($"{p.Key}={Uri.EscapeDataString(p.Value)}");
});
return new Uri(_baseUrl, request.Resource + queryStringBuilder.ToString());
}
- public async Task ExecuteHttpRequestAsync(RestRequest request)
+ internal async Task ExecuteHttpRequestAsync(RestRequest request)
{
var uri = BuildUri(request);
diff --git a/src/Rest/RestRequest.cs b/src/Rest/RestRequest.cs
index deb5815..9f3f5ae 100644
--- a/src/Rest/RestRequest.cs
+++ b/src/Rest/RestRequest.cs
@@ -11,22 +11,22 @@ internal RestRequest(string resource, HttpMethod method)
Resource = resource;
}
- public Dictionary QueryParameters { get; private set; } = new Dictionary();
- public Dictionary Headers { get; private set; } = new Dictionary();
- public HttpMethod Method { get; private set; }
- public string Resource { get; private set; }
- public string JsonBody { get; private set; }
- public System.IO.Stream FileStream { get; private set; }
- public string FileStreamContentType { get; private set; }
- public string FileStreamName { get; private set; }
+ internal Dictionary QueryParameters { get; private set; } = new Dictionary();
+ internal Dictionary Headers { get; private set; } = new Dictionary();
+ internal HttpMethod Method { get; private set; }
+ internal string Resource { get; private set; }
+ internal string JsonBody { get; private set; }
+ internal System.IO.Stream FileStream { get; private set; }
+ internal string FileStreamContentType { get; private set; }
+ internal string FileStreamName { get; private set; }
- public void AddHeader(string name, string value) => Headers[name] = value;
+ internal void AddHeader(string name, string value) => Headers[name] = value;
- public void AddQueryParameter(string name, string value) => QueryParameters[name] = value;
+ internal void AddQueryParameter(string name, string value) => QueryParameters[name] = value;
- public void SetJsonBody(string json) => JsonBody = json;
+ internal void SetJsonBody(string json) => JsonBody = json;
- public void SetFileStream(System.IO.Stream stream, string name, string contentType)
+ internal void SetFileStream(System.IO.Stream stream, string name, string contentType)
{
FileStream = stream;
FileStreamName = name;
diff --git a/src/Rest/RestResponse.cs b/src/Rest/RestResponse.cs
index d4e3d4c..89f26f6 100644
--- a/src/Rest/RestResponse.cs
+++ b/src/Rest/RestResponse.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Net;
+using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
@@ -7,9 +6,9 @@ namespace Stream.Rest
{
internal class RestResponse
{
- public HttpStatusCode StatusCode { get; set; }
+ internal HttpStatusCode StatusCode { get; set; }
- public string Content { get; set; }
+ internal string Content { get; set; }
internal static async Task FromResponseMessage(HttpResponseMessage message)
{
diff --git a/src/Utils/ActivityIdGenerator.cs b/src/Utils/ActivityIdGenerator.cs
new file mode 100644
index 0000000..b590d7f
--- /dev/null
+++ b/src/Utils/ActivityIdGenerator.cs
@@ -0,0 +1,130 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace Stream.Utils
+{
+ /// Utility class to generate a unique activity id.
+ public static class ActivityIdGenerator
+ {
+ private static DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ private static long EpochTicks = Epoch.Ticks;
+
+ // Difference in 100-nanosecond intervals between
+ // UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970)
+ private static long UuidEpochDifference = 122192928000000000;
+
+ /// Generates an Activity ID for the given epoch timestamp and foreign ID.
+ public static Guid GenerateId(int epoch, string foreignId)
+ {
+ return GenerateId(Epoch.AddSeconds(epoch), foreignId);
+ }
+
+ /// Generates an Activity ID for the given timestamp and foreign ID.
+ public static Guid GenerateId(DateTime timestamp, string foreignId)
+ {
+ var unixNano = timestamp.Ticks - EpochTicks;
+ var t = (ulong)(UuidEpochDifference + unixNano);
+
+ long signedDigest;
+ using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(foreignId)))
+ {
+ var hashAsInt = Murmur3.Hash(stream);
+
+ if (hashAsInt > int.MaxValue)
+ signedDigest = (long)hashAsInt - 4294967296;
+ else
+ signedDigest = hashAsInt;
+ }
+
+ long signMask;
+ if (signedDigest > 0)
+ {
+ signMask = 0x100000000;
+ }
+ else
+ {
+ signedDigest *= -1;
+ signMask = 0x000000000;
+ }
+
+ signedDigest = (signedDigest | signMask) | 0x800000000000;
+
+ var nodeBytes = PutUint64((ulong)signedDigest);
+
+ var finalBytes = new byte[16];
+ Array.Copy(PutUint32(TrimUlongToUint(t)), 0, finalBytes, 0, 4);
+ Array.Copy(PutUint16(TrimUlongToUshort(t >> 32)), 0, finalBytes, 4, 2);
+ Array.Copy(PutUint16((ushort)(0x1000 | TrimUlongToUshort(t >> 48))), 0, finalBytes, 6, 2);
+ Array.Copy(PutUint16(TrimUlongToUshort(0x8080)), 0, finalBytes, 8, 2);
+
+ // Now to the final
+ Array.Copy(nodeBytes, 2, finalBytes, 10, 6);
+
+ return new Guid(BytesToGuidString(finalBytes));
+ }
+
+ private static byte[] PutUint64(ulong v)
+ {
+ var b = new byte[8];
+ b[0] = (byte)((v >> 56) & 0xFF);
+ b[1] = (byte)((v >> 48) & 0xFF);
+ b[2] = (byte)((v >> 40) & 0xFF);
+ b[3] = (byte)((v >> 32) & 0xFF);
+ b[4] = (byte)((v >> 24) & 0xFF);
+ b[5] = (byte)((v >> 16) & 0xFF);
+ b[6] = (byte)((v >> 8) & 0xFF);
+ b[7] = (byte)(v & 0xFF);
+
+ return b;
+ }
+
+ private static byte[] PutUint32(uint v)
+ {
+ var b = new byte[4];
+ b[0] = (byte)((v >> 24) & 0xFF);
+ b[1] = (byte)((v >> 16) & 0xFF);
+ b[2] = (byte)((v >> 8) & 0xFF);
+ b[3] = (byte)(v & 0xFF);
+
+ return b;
+ }
+
+ private static byte[] PutUint16(ushort v)
+ {
+ var b = new byte[2];
+ b[0] = (byte)((v >> 8) & 0xFF);
+ b[1] = (byte)(v & 0xFF);
+
+ return b;
+ }
+
+ private static uint TrimUlongToUint(ulong l)
+ {
+ return (uint)(l & 0xFFFFFFFF);
+ }
+
+ private static ushort TrimUlongToUshort(ulong l)
+ {
+ return (ushort)(l & 0xFFFF);
+ }
+
+ private static string BytesToGuidString(byte[] b)
+ {
+ var offsets = new int[16] { 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 };
+ var hexString = "0123456789abcdef";
+ var retVal = new byte[36];
+ for (var i = 0; i < b.Length; i++)
+ {
+ var value = b[i];
+ retVal[offsets[i]] = (byte)hexString[value >> 4];
+ retVal[offsets[i] + 1] = (byte)hexString[value & 0xF];
+ }
+
+ const int dash = 45; // The dash character '-'
+ retVal[8] = retVal[13] = retVal[18] = retVal[23] = dash;
+
+ return Encoding.UTF8.GetString(retVal);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Utils/Extensions.cs b/src/Utils/Extensions.cs
index 1e3aebb..5b5ab57 100644
--- a/src/Utils/Extensions.cs
+++ b/src/Utils/Extensions.cs
@@ -8,7 +8,7 @@ internal static class Extensions
{
internal static void ForEach(this IEnumerable items, Action action)
{
- if ((items == null) || (action == null))
+ if (items == null || action == null)
return;
foreach (var item in items)
diff --git a/src/Utils/Murmur3.cs b/src/Utils/Murmur3.cs
new file mode 100644
index 0000000..db1815f
--- /dev/null
+++ b/src/Utils/Murmur3.cs
@@ -0,0 +1,98 @@
+using System.IO;
+using IoStream = System.IO.Stream;
+
+namespace Stream.Utils
+{
+ /*
+ Copied from https://gist.githubusercontent.com/automatonic/3725443/raw/c2ffc51ed8e9ee3c89e8016c062672d3d52ef999/MurMurHash3.cs
+ The only change is that we set the Seed value to zero to match the backend Go implementation.
+ */
+ internal static class Murmur3
+ {
+ // Change to suit your needs
+ private const uint Seed = 0;
+
+ internal static int Hash(IoStream stream)
+ {
+ const uint c1 = 0xcc9e2d51;
+ const uint c2 = 0x1b873593;
+
+ uint h1 = Seed;
+ uint k1 = 0;
+ uint streamLength = 0;
+
+ using (BinaryReader reader = new BinaryReader(stream))
+ {
+ byte[] chunk = reader.ReadBytes(4);
+ while (chunk.Length > 0)
+ {
+ streamLength += (uint)chunk.Length;
+ switch (chunk.Length)
+ {
+ case 4:
+ /* Get four bytes from the input into an uint */
+ k1 = (uint)(chunk[0] | chunk[1] << 8 | chunk[2] << 16 | chunk[3] << 24);
+
+ /* bitmagic hash */
+ k1 *= c1;
+ k1 = Rotl32(k1, 15);
+ k1 *= c2;
+
+ h1 ^= k1;
+ h1 = Rotl32(h1, 13);
+ h1 = (h1 * 5) + 0xe6546b64;
+ break;
+ case 3:
+ k1 = (uint)(chunk[0] | chunk[1] << 8 | chunk[2] << 16);
+ k1 *= c1;
+ k1 = Rotl32(k1, 15);
+ k1 *= c2;
+ h1 ^= k1;
+ break;
+ case 2:
+ k1 = (uint)(chunk[0] | chunk[1] << 8);
+ k1 *= c1;
+ k1 = Rotl32(k1, 15);
+ k1 *= c2;
+ h1 ^= k1;
+ break;
+ case 1:
+ k1 = chunk[0];
+ k1 *= c1;
+ k1 = Rotl32(k1, 15);
+ k1 *= c2;
+ h1 ^= k1;
+ break;
+ }
+
+ chunk = reader.ReadBytes(4);
+ }
+ }
+
+ // finalization, magic chants to wrap it all up
+ h1 ^= streamLength;
+ h1 = Fmix(h1);
+
+ // ignore overflow
+ unchecked
+ {
+ return (int)h1;
+ }
+ }
+
+ private static uint Rotl32(uint x, byte r)
+ {
+ return (x << r) | (x >> (32 - r));
+ }
+
+ private static uint Fmix(uint h)
+ {
+ h ^= h >> 16;
+ h *= 0x85ebca6b;
+ h ^= h >> 13;
+ h *= 0xc2b2ae35;
+ h ^= h >> 16;
+ return h;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Credentials.cs b/tests/Credentials.cs
new file mode 100644
index 0000000..ee151a3
--- /dev/null
+++ b/tests/Credentials.cs
@@ -0,0 +1,23 @@
+using Stream;
+using System;
+
+namespace StreamNetTests
+{
+ public class Credentials
+ {
+ internal Credentials()
+ {
+ Client = new StreamClient(
+ Environment.GetEnvironmentVariable("STREAM_API_KEY"),
+ Environment.GetEnvironmentVariable("STREAM_API_SECRET"),
+ new StreamClientOptions
+ {
+ Location = StreamApiLocation.USEast,
+ Timeout = 16000,
+ });
+ }
+
+ public static Credentials Instance { get; } = new Credentials();
+ public StreamClient Client { get; }
+ }
+}
\ No newline at end of file
diff --git a/tests/ReactionTests.cs b/tests/ReactionTests.cs
index 81981a7..aaffcde 100644
--- a/tests/ReactionTests.cs
+++ b/tests/ReactionTests.cs
@@ -125,7 +125,7 @@ public async Task TestReactionPagination()
var r3 = await Client.Reactions.AddAsync("like", activity.Id, "bob", data);
var r4 = await Client.Reactions.AddChildAsync(r3, "upvote", "tom", data);
- var r5 = await Client.Reactions.AddChildAsync(r3, "upvote", "mary", data);
+ var r5 = await Client.Reactions.AddChildAsync(r3, Guid.NewGuid().ToString(), "upvote", "mary", data);
// activity id
var filter = ReactionFiltering.Default;
diff --git a/tests/UtilsTests.cs b/tests/UtilsTests.cs
index 31ae4ad..d9f1da2 100644
--- a/tests/UtilsTests.cs
+++ b/tests/UtilsTests.cs
@@ -1,23 +1,54 @@
-using Stream;
+using NUnit.Framework;
+using Stream.Models;
+using Stream.Utils;
using System;
+using System.Threading.Tasks;
namespace StreamNetTests
{
- public class Credentials
+ [TestFixture]
+ public class UtilsTests : TestBase
{
- internal Credentials()
+ [Test]
+ public void TestIdGenerator()
{
- Client = new StreamClient(
- Environment.GetEnvironmentVariable("STREAM_API_KEY"),
- Environment.GetEnvironmentVariable("STREAM_API_SECRET"),
- new StreamClientOptions
- {
- Location = StreamApiLocation.USEast,
- Timeout = 16000,
- });
+ // All these test cases are copied from the backend Go implementation
+ var first = ActivityIdGenerator.GenerateId(1451260800, "123sdsd333}}}");
+ Assert.AreEqual("f01c0000-acf5-11e5-8080-80006a8b5bc2", first.ToString());
+
+ var second = ActivityIdGenerator.GenerateId(1452862482, "6621934");
+ Assert.AreEqual("24f9d500-bb87-11e5-8080-80012ce3cc51", second.ToString());
+
+ var third = ActivityIdGenerator.GenerateId(1452862489, "6621938");
+ Assert.AreEqual("2925f280-bb87-11e5-8080-8001597d791f", third.ToString());
+
+ var fourth = ActivityIdGenerator.GenerateId(1452862492, "6621941");
+ Assert.AreEqual("2aefb600-bb87-11e5-8080-8001226fe2f2", fourth.ToString());
+
+ var fifth = ActivityIdGenerator.GenerateId(1452862496, "6621945");
+ Assert.AreEqual("2d521000-bb87-11e5-8080-800026259780", fifth.ToString());
+
+ var sixth = ActivityIdGenerator.GenerateId(1463914800, "top_issues_summary_557dc1d9e46fea0a4c000002");
+ Assert.AreEqual("53dbf800-200c-11e6-8080-800023ec2877", sixth.ToString());
+
+ var seventh = ActivityIdGenerator.GenerateId(1452866055, "6625387");
+ Assert.AreEqual("76a65d80-bb8f-11e5-8080-8000530672db", seventh.ToString());
+
+ var eight = ActivityIdGenerator.GenerateId(1480174117, "UserPrediction:11127278");
+ Assert.AreEqual("00000080-b3ed-11e6-8080-8000444ef374", eight.ToString());
+
+ var nineth = ActivityIdGenerator.GenerateId(1481475921, "467791-42-follow");
+ Assert.AreEqual("ffa65e80-bfc3-11e6-8080-800027086507", nineth.ToString());
}
- public static Credentials Instance { get; } = new Credentials();
- public StreamClient Client { get; }
+ [Test]
+ public async Task TestActivityIdSameAsBackend()
+ {
+ var foreignId = Guid.NewGuid().ToString();
+ var inputAct = new Activity("1", "test", "1") { Time = DateTime.UtcNow, ForeignId = foreignId };
+ var activity = await this.UserFeed.AddActivityAsync(inputAct);
+
+ Assert.AreEqual(ActivityIdGenerator.GenerateId(activity.Time.Value, foreignId).ToString(), activity.Id);
+ }
}
}