Skip to content

Commit 1eb814b

Browse files
authored
Add personalization (#33)
1 parent d50e2b4 commit 1eb814b

File tree

8 files changed

+181
-40
lines changed

8 files changed

+181
-40
lines changed

README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ var activities = await client.Batch.GetActivities(ids)
8484
var foreignIDTimes = new ForeignIDTime[] {new ForeignIDTime("fid-1", DateTime.Parse("2000-08-19T16:32:32")), new Stream.ForeignIDTime("fid-2", DateTime.Parse("2000-08-21T16:32:32"))};
8585
var activities = await client.Batch.GetActivities(null, foreignIDTimes)
8686

87-
//Partially update an activity
87+
// Partially update an activity
8888
var set = new GenericData();
8989
set.SetData("custom_field", "new value");
9090
var unset = new string[]{"field to remove"};
9191

92-
//by id
92+
// by id
9393
await client.ActivityPartialUpdate("e561de8f-00f1-11e4-b400-0cc47a024be0", null, set, unset);
9494

95-
//by foreign id and time
95+
// by foreign id and time
9696
var fidTime = new ForeignIDTime("fid-1", DateTime.Parse("2000-08-19T16:32:32"));
9797
await client.ActivityPartialUpdate(null, fidTime, set, unset);
9898

@@ -106,10 +106,10 @@ var activity = await userFeed1.AddActivity(activity);
106106

107107
var r = await client.Reactions.Add("comment", activity.Id, "john");
108108

109-
//add a reaction to a reaction
109+
// add a reaction to a reaction
110110
var child = await client.Reactions.AddChild(r.ID, "upvote", activity.Id, "john");
111111

112-
//enrich feed results
112+
// enrich feed results
113113
var userData = new Dictionary<string, object>()
114114
{
115115
{"is_admin", true},
@@ -121,7 +121,7 @@ var userRef = u.Ref();
121121
var a = new Stream.Activity(uRef, "add", "post");
122122
var plainActivity = await userFeed1.AddActivity(a);
123123

124-
//plainActivity.Actor is just a plain string containing the user ref
124+
// plainActivity.Actor is just a plain string containing the user ref
125125
126126
var enriched = await this._user1.GetEnrichedFlatActivities();
127127
var actor = enriched.Results.First();
@@ -133,7 +133,7 @@ if (actor.IsEnriched)
133133
}
134134

135135

136-
//enrich feed results with reactions
136+
// enrich feed results with reactions
137137
138138
var activityData = new Activity("bob", "cook", "burger")
139139
{
@@ -161,6 +161,16 @@ Console.WriteLine(enrichedActivity.OwnReactions["comment"]); // is the $comment
161161
162162
// all reactions enrichment can be selected
163163
var enriched = await this._user1.GetEnrichedFlatActivities(GetOptions.Default.WithReaction(ReactionOption.With().Counts().Own().Recent()));
164+
165+
// Personalization
166+
var input = new Dictionary<string, object>()
167+
{
168+
{"feed_slug", "my_personalized_feed"},
169+
{"user_id", "john"},
170+
{"limit", 20},
171+
{"ranking", "my_ranking"}
172+
};
173+
var response = await client.Personalization.Get("my_endpoint", input);
164174
```
165175

166176
### Copyright and License Information

src/stream-net-tests/IntegrationTests.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1875,10 +1875,6 @@ public async Task TestBatchPartialUpdate()
18751875

18761876
Assert.IsNull(updatedActs[0].GetData<string>("custom_thing3"));
18771877
var extraData = updatedActs[2].GetData<Dictionary<string, string>>("details");
1878-
if (extraData == null)
1879-
{
1880-
Console.WriteLine("IT'S NULL " + updatedActs[2].ForeignId);
1881-
}
18821878
Assert.AreEqual("nowhere", extraData["address"]);
18831879

18841880
//ID
@@ -2410,5 +2406,34 @@ public async Task TestEnrich()
24102406
Assert.True(enrichedAct.ReactionCounts.ContainsKey(reaction.Kind));
24112407
Assert.AreEqual(1, enrichedAct.ReactionCounts[reaction.Kind]);
24122408
}
2409+
2410+
[Test]
2411+
[Ignore("Not always needed, set credentials to run when needed")]
2412+
public async Task ReadPersonalization()
2413+
{
2414+
var _p = new Stream.StreamClient(
2415+
"some_key",
2416+
"some_secret",
2417+
new Stream.StreamClientOptions()
2418+
{
2419+
Location = Stream.StreamApiLocation.Dublin,
2420+
PersonalizationLocation = Stream.StreamApiLocation.USEast,
2421+
Timeout = 10000,
2422+
PersonalizationTimeout = 10000
2423+
});
2424+
2425+
var response = await _p.Personalization.Get("etoro_newsfeed", new Dictionary<string, object>()
2426+
{
2427+
{"feed_slug", "newsfeed"},
2428+
{"user_id", "crembo"},
2429+
{"limit", 20},
2430+
{"ranking", "etoro"}
2431+
});
2432+
2433+
var d = new Dictionary<string, object>(response);
2434+
Assert.AreEqual(41021, d["app_id"]);
2435+
Assert.True(d.ContainsKey("duration"));
2436+
Assert.True(d.ContainsKey("results"));
2437+
}
24132438
}
24142439
}

src/stream-net/Personalization.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using Newtonsoft.Json;
2+
using Newtonsoft.Json.Linq;
3+
using Stream.Rest;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
9+
namespace Stream
10+
{
11+
public class Personalization
12+
{
13+
readonly StreamClient _client;
14+
15+
internal Personalization(StreamClient client)
16+
{
17+
_client = client;
18+
}
19+
20+
public async Task<IDictionary<string, object>> Get(string endpoint, IDictionary<string, object> data)
21+
{
22+
var request = this._client.BuildPersonalizationRequest(endpoint + "/", HttpMethod.GET);
23+
foreach(KeyValuePair<string, object> entry in data)
24+
{
25+
request.AddQueryParameter(entry.Key, Convert.ToString(entry.Value));
26+
}
27+
28+
var response = await this._client.MakeRequest(request);
29+
if ((int)response.StatusCode < 300)
30+
return JsonConvert.DeserializeObject<Dictionary<string, object>>(response.Content);
31+
32+
throw StreamException.FromResponse(response);
33+
}
34+
35+
public async Task<IDictionary<string, object>> Post(string endpoint, IDictionary<string, object> data)
36+
{
37+
var request = this._client.BuildJWTAppRequest(endpoint + "/", HttpMethod.POST);
38+
request.SetJsonBody(JsonConvert.SerializeObject(data));
39+
40+
var response = await this._client.MakeRequest(request);
41+
if ((int)response.StatusCode < 300)
42+
return JsonConvert.DeserializeObject<Dictionary<string, object>>(response.Content);
43+
44+
throw StreamException.FromResponse(response);
45+
}
46+
47+
public async Task Delete(string endpoint, IDictionary<string, object> data)
48+
{
49+
var request = this._client.BuildJWTAppRequest(endpoint + "/", HttpMethod.DELETE);
50+
foreach(KeyValuePair<string, object> entry in data)
51+
{
52+
request.AddQueryParameter(entry.Key, Convert.ToString(entry.Value));
53+
}
54+
55+
var response = await this._client.MakeRequest(request);
56+
if ((int)response.StatusCode >= 300)
57+
throw StreamException.FromResponse(response);
58+
}
59+
};
60+
}

src/stream-net/Rest/RestRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal RestRequest(string resource, HttpMethod method)
2525
public string Resource { get; private set; }
2626

2727
public string JsonBody { get; private set; }
28-
28+
2929
public void AddHeader(string name, string value)
3030
{
3131
_headers[name] = value;

src/stream-net/Rest/RestResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal static async Task<RestResponse> FromResponseMessage(HttpResponseMessage
2424
{
2525
StatusCode = message.StatusCode
2626
};
27-
27+
2828
response.Content = await message.Content.ReadAsStringAsync();
2929

3030
return response;

src/stream-net/StreamClient.cs

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public class StreamClient : IStreamClient
1313
{
1414
internal const string BaseUrlFormat = "https://{0}-api.stream-io-api.com";
1515
internal const string BaseUrlPath = "/api/v1.0/";
16+
internal const string BasePersonalizationUrlFormat = "https://{0}-personalization.stream-io-api.com";
17+
internal const string BasePersonalizationUrlPath = "/personalization/v1.0/";
1618
internal const string ActivitiesUrlPath = "activities/";
1719
internal const int ActivityCopyLimitDefault = 300;
1820
internal const int ActivityCopyLimitMax = 1000;
@@ -38,9 +40,23 @@ public StreamClient(string apiKey, string apiSecret, StreamClientOptions options
3840
_apiKey = apiKey;
3941
_apiSecret = apiSecret;
4042
_options = options ?? StreamClientOptions.Default;
41-
_client = new RestClient(GetBaseUrl(), TimeSpan.FromMilliseconds(_options.Timeout));
43+
_client = new RestClient(GetBaseUrl(_options.Location), TimeSpan.FromMilliseconds(_options.Timeout));
4244
}
4345

46+
private StreamClient(string apiKey, string apiSecret, RestClient client, StreamClientOptions options = null)
47+
{
48+
if (string.IsNullOrWhiteSpace(apiKey))
49+
throw new ArgumentNullException("apiKey", "Must have an apiKey");
50+
if (string.IsNullOrWhiteSpace(apiSecret))
51+
throw new ArgumentNullException("apiSecret", "Must have an apiSecret");
52+
53+
_apiKey = apiKey;
54+
_apiSecret = apiSecret;
55+
_options = options ?? StreamClientOptions.Default;
56+
_client = client;
57+
}
58+
59+
4460
/// <summary>
4561
/// Get a feed
4662
/// </summary>
@@ -124,39 +140,50 @@ public Users Users
124140
}
125141
}
126142

127-
private Uri GetBaseUrl()
143+
public Personalization Personalization
128144
{
129-
string region = "";
130-
switch (_options.Location)
145+
get
146+
{
147+
var _personalization = new RestClient(GetBasePersonalizationUrl(_options.PersonalizationLocation), TimeSpan.FromMilliseconds(_options.PersonalizationTimeout));
148+
return new Personalization(new StreamClient(_apiKey, _apiSecret, _personalization, _options));
149+
}
150+
}
151+
152+
private Uri GetBaseUrl(StreamApiLocation location)
153+
{
154+
return new Uri(string.Format(BaseUrlFormat, GetRegion(_options.Location)));
155+
}
156+
157+
private Uri GetBasePersonalizationUrl(StreamApiLocation location)
158+
{
159+
return new Uri(string.Format(BasePersonalizationUrlFormat, GetRegion(_options.PersonalizationLocation)));
160+
}
161+
162+
private string GetRegion(StreamApiLocation location)
163+
{
164+
switch (location)
131165
{
132166
case StreamApiLocation.USEast:
133-
region = "us-east";
134-
break;
167+
return "us-east";
135168
case StreamApiLocation.Tokyo:
136-
region = "tokyo";
137-
break;
169+
return "tokyo";
138170
case StreamApiLocation.Dublin:
139-
region = "dublin";
140-
break;
171+
return "dublin";
141172
case StreamApiLocation.Singapore:
142-
region = "singapore";
143-
break;
173+
return "singapore";
144174
case StreamApiLocation.USWest:
145-
region = "us-west";
146-
break;
175+
return "us-west";
147176
case StreamApiLocation.EUCentral:
148-
region = "eu-central";
149-
break;
177+
return "eu-central";
150178
default:
151-
break;
179+
return "us-east";
152180
}
153-
return new Uri(string.Format(BaseUrlFormat, region));
154181
}
155182

156-
private RestRequest BuildRestRequest(string fullPath, HttpMethod method)
183+
private RestRequest BuildRestRequest(string fullPath, HttpMethod method, string userID = null)
157184
{
158185
var request = new RestRequest(fullPath, method);
159-
request.AddHeader("Authorization", JWToken("*"));
186+
request.AddHeader("Authorization", JWToken("*", userID));
160187
request.AddHeader("stream-auth-type", "jwt");
161188
request.AddQueryParameter("api_key", _apiKey);
162189
return request;
@@ -189,6 +216,11 @@ internal RestRequest BuildAppRequest(string path, HttpMethod method)
189216
return request;
190217
}
191218

219+
internal RestRequest BuildPersonalizationRequest(string path, HttpMethod method)
220+
{
221+
return BuildRestRequest(BasePersonalizationUrlPath + path, method, "*");
222+
}
223+
192224
internal void SignRequest(RestRequest request)
193225
{
194226
// make signature
@@ -237,14 +269,18 @@ internal string Sign256(string feedId)
237269
return Convert.ToBase64String(hmac.ComputeHash(encoding.GetBytes(feedId)));
238270
}
239271

240-
internal string JWToken(string feedId)
272+
internal string JWToken(string feedId, string userID = null)
241273
{
242-
var payload = new
274+
var payload = new Dictionary<string, string>()
243275
{
244-
resource = "*",
245-
action = "*",
246-
feed_id = feedId
276+
{"resource", "*"},
277+
{"action", "*"},
278+
{"feed_id", feedId}
247279
};
280+
if (userID != null)
281+
{
282+
payload["user_id"] = userID;
283+
}
248284
return this.JWToken(payload);
249285
}
250286

src/stream-net/StreamClientOptions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,25 @@ public class StreamClientOptions
2525
/// <remarks>Default is 3000</remarks>
2626
public int Timeout { get; set; }
2727

28+
/// <summary>
29+
/// Number of milliseconds to wait on requests to personalization
30+
/// </summary>
31+
/// <remarks>Default is 3000</remarks>
32+
public int PersonalizationTimeout { get; set; }
33+
2834
public StreamApiLocation Location { get; set; }
2935

36+
public StreamApiLocation PersonalizationLocation { get; set; }
37+
3038
public bool ExpireTokens { get; set; }
3139

3240
public StreamClientOptions()
3341
{
3442
ExpireTokens = false;
3543
Location = StreamApiLocation.USEast;
3644
Timeout = 3000;
45+
PersonalizationTimeout = 3000;
46+
PersonalizationLocation = StreamApiLocation.USEast;
3747
}
3848
}
3949
}

src/stream-net/StreamException.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class StreamException : Exception
1313
internal StreamException(ExceptionState state)
1414
: base(message: state.Detail)
1515
{
16-
16+
1717
}
1818

1919
internal class ExceptionState
@@ -30,7 +30,7 @@ internal class ExceptionState
3030

3131
internal static StreamException FromResponse(RestResponse response)
3232
{
33-
//If we get an error response from getstream.io with the following structure then use it to populate the exception details,
33+
//If we get an error response from getstream.io with the following structure then use it to populate the exception details,
3434
//otherwise fill in the properties from the response, the most likely case being when we get a timeout.
3535
//{"code": 6, "detail": "The following feeds are not configured: 'secret'", "duration": "4ms", "exception": "FeedConfigException", "status_code": 400}
3636

0 commit comments

Comments
 (0)