-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathRecombeeClient.cs
More file actions
313 lines (273 loc) · 11.8 KB
/
RecombeeClient.cs
File metadata and controls
313 lines (273 loc) · 11.8 KB
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
using System;
using System.Security.Cryptography;
using System.Text;
using System.Net;
using System.Net.Http;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Recombee.ApiClient.ApiRequests;
using Recombee.ApiClient.Bindings;
namespace Recombee.ApiClient
{
/// <summary>Client for sending requests to Recombee and getting replies</summary>
public partial class RecombeeClient
{
readonly string databaseId;
readonly byte[] secretTokenBytes;
readonly bool useHttpsAsDefault;
private readonly HttpMessageHandler handler;
readonly string hostUri = "rapi.recombee.com";
readonly int BATCH_MAX_SIZE = 10000; //Maximal number of requests within one batch request
HttpClient httpClient;
/// <summary>Initialize the client</summary>
/// <param name="databaseId">ID of the database.</param>
/// <param name="secretToken">Corresponding secret token.</param>
/// <param name="useHttpsAsDefault">If true, all requests are sent using HTTPS</param>
/// <param name="handler">Optional message handler for the http client</param>
public RecombeeClient(string databaseId, string secretToken, bool useHttpsAsDefault = true, HttpMessageHandler handler = null)
{
this.databaseId = databaseId;
this.secretTokenBytes = Encoding.ASCII.GetBytes(secretToken);
this.useHttpsAsDefault = useHttpsAsDefault;
this.handler = handler;
this.httpClient = createHttpClient();
var envHostUri = Environment.GetEnvironmentVariable("RAPI_URI");
if(envHostUri != null)
this.hostUri = envHostUri;
}
private HttpClient createHttpClient()
{
var httpClient = handler == null ? new HttpClient() : new HttpClient(handler);
httpClient.DefaultRequestHeaders.Add("User-Agent", "recombee-.net-api-client/2.4.1");
return httpClient;
}
public StringBinding Send(Request request)
{
var json = SendRequest(request);
return ParseResponse(json, request);
}
public StringBinding ParseResponse(string json, Request request)
{
return new StringBinding(json);
}
public IEnumerable<Item> Send(ListItems request)
{
var json = SendRequest(request);
return ParseResponse(json, request);
}
public IEnumerable<User> Send(ListUsers request)
{
var json = SendRequest(request);
return ParseResponse(json, request);
}
public IEnumerable<Recommendation> Send(UserBasedRecommendation request)
{
var json = SendRequest(request);
return ParseResponse(json, request);
}
public IEnumerable<Recommendation> Send(ItemBasedRecommendation request)
{
var json = SendRequest(request);
return ParseResponse(json, request);
}
private IEnumerable<Recommendation> ParseResponse(string json, UserBasedRecommendation request)
{
return ParseRecommRequestResponse(json, request);
}
private IEnumerable<Recommendation> ParseResponse(string json, ItemBasedRecommendation request)
{
return ParseRecommRequestResponse(json, request);
}
private IEnumerable<Recommendation> ParseRecommRequestResponse(string json, Request request)
{
try
{
var strArray = JsonConvert.DeserializeObject<string[]>(json);
return strArray.Select(x => new Recommendation(x));
}
catch(Newtonsoft.Json.JsonReaderException)
{
//might have failed because it returned also the item properties
var valsArray = JsonConvert.DeserializeObject<Dictionary<string, object>[]>(json);
return valsArray.Select(vals => new Recommendation((string)vals["itemId"], vals));
}
}
private IEnumerable<Item> ParseResponse(string json, ListItems request)
{
try
{
var strArray = JsonConvert.DeserializeObject<string[]>(json);
return strArray.Select(x => new Item(x));
}
catch(Newtonsoft.Json.JsonReaderException)
{
//might have failed because it returned also the item properties
var valsArray = JsonConvert.DeserializeObject<Dictionary<string, object>[]>(json);
return valsArray.Select(vals => new Item((string)vals["itemId"], vals));
}
}
private IEnumerable<User> ParseResponse(string json, ListUsers request)
{
try
{
var strArray = JsonConvert.DeserializeObject<string[]>(json);
return strArray.Select(x => new User(x));
}
catch(Newtonsoft.Json.JsonReaderException)
{
//might have failed because it returned also the item properties
var valsArray = JsonConvert.DeserializeObject<Dictionary<string, object>[]>(json);
return valsArray.Select(vals => new User((string)vals["userId"], vals));
}
}
public Item Send(GetItemValues request)
{
var json = SendRequest(request);
return ParseResponse(json, request);
}
private Item ParseResponse(string json, GetItemValues request)
{
var vals = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
return new Item(request.ItemId, vals);
}
public User Send(GetUserValues request)
{
var json = SendRequest(request);
return ParseResponse(json, request);
}
private User ParseResponse(string json, GetUserValues request)
{
var vals = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
return new User(request.UserId, vals);
}
private class BatchParseHelper
{
public int code;
public Newtonsoft.Json.Linq.JRaw json;
public BatchParseHelper()
{
code = 0;
json = null;
}
}
public BatchResponse Send(Batch request)
{
if(request.Requests.Count() > BATCH_MAX_SIZE )
return SendMultipartBatchRequest(request);
var json = SendRequest(request);
var partiallyParsed = JsonConvert.DeserializeObject<BatchParseHelper[]>(json);
var resps = request.Requests.Zip(partiallyParsed, (req, res) => (object) ParseOneBatchResponse(res.json.ToString(), res.code, req));
var statusCodes = partiallyParsed.Select(res => (HttpStatusCode) res.code);
return new BatchResponse(resps, statusCodes);
}
private BatchResponse SendMultipartBatchRequest(Batch request)
{
var parts = request.Requests.Part(BATCH_MAX_SIZE);
var results = parts.Select(reqs => Send(new Batch(reqs)));
var responses = results.Select(br => br.Responses).SelectMany(x => x);
var statusCodes = results.Select(br => br.StatusCodes).SelectMany(x => x);
return new BatchResponse(responses, statusCodes);
}
protected string SendRequest(Request request)
{
var uri = ProcessRequestUri(request);
try
{
HttpResponseMessage response = PerformHTTPRequest(uri, request);
var jsonStringTask = response.Content.ReadAsStringAsync();
jsonStringTask.Wait();
var jsonString = jsonStringTask.Result;
CheckStatusCode(response.StatusCode, jsonString, request);
return jsonString;
}
catch(AggregateException ae)
{
ae.Handle((x) =>
{
if(x is System.Threading.Tasks.TaskCanceledException)
throw new TimeoutException(request, x);
return false;
});
}
throw new InvalidOperationException("Invalid state after sending a request."); //Should never happen
}
private void CheckStatusCode(System.Net.HttpStatusCode statusCode, string response, Request request)
{
int code = (int) statusCode;
if(code>=200 && code <=299)
return;
throw new ResponseException(request, statusCode, response);
}
private HttpResponseMessage PerformHTTPRequest(string uri, Request request)
{
if (httpClient.Timeout != request.Timeout)
{
if (httpClient.Timeout != null)
httpClient = createHttpClient();
httpClient.Timeout = request.Timeout;
}
if (request.RequestHttpMehod == HttpMethod.Get)
{
return httpClient.GetAsync(uri).Result;
}
else if (request.RequestHttpMehod == HttpMethod.Put)
{
return httpClient.PutAsync(uri, new StringContent("")).Result;
}
else if (request.RequestHttpMehod == HttpMethod.Post)
{
string bodyParams = JsonConvert.SerializeObject(request.BodyParameters());
return httpClient.PostAsync(uri, new StringContent(bodyParams, Encoding.UTF8,"application/json")).Result;
}
else if (request.RequestHttpMehod == HttpMethod.Delete)
{
return httpClient.DeleteAsync(uri).Result;
}
else
{
throw new InvalidOperationException("Unknown method " + request.RequestHttpMehod.ToString());
}
}
private string StripLeadingQuestionMark(string query)
{
if (query.Length > 0 && query[0] == '?')
{
return query.Substring(1);
}
return query;
}
private string ProcessRequestUri(Request request)
{
var uriBuilder = new UriBuilder();
uriBuilder.Path = string.Format("/{0}{1}",databaseId,request.Path());
uriBuilder.Query = QueryParameters(request);
AppendHmacParameters(uriBuilder);
uriBuilder.Scheme = (request.EnsureHttps || useHttpsAsDefault) ? "https" : "http";
uriBuilder.Host = hostUri;
return uriBuilder.ToString();
}
private string QueryParameters(Request request)
{
var encodedQueryStringParams = request.QueryParameters().
Select (p => string.Format("{0}={1}", p.Key, WebUtility.UrlEncode(String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}", p.Value))));
return string.Join("&", encodedQueryStringParams);
}
private Int32 UnixTimestampNow()
{
return (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
}
private void AppendHmacParameters(UriBuilder uriBuilder)
{
uriBuilder.Query = StripLeadingQuestionMark(uriBuilder.Query) + (uriBuilder.Query.Length == 0 ? "" : "&") + string.Format("hmac_timestamp={0}",this.UnixTimestampNow());
using(var myhmacsha1 = new HMACSHA1(this.secretTokenBytes))
{
var uriToBeSigned = uriBuilder.Path + uriBuilder.Query;
byte[] hmacBytes = myhmacsha1.ComputeHash(Encoding.ASCII.GetBytes(uriToBeSigned));
string hmacRes = BitConverter.ToString(hmacBytes).Replace("-","");
uriBuilder.Query = StripLeadingQuestionMark(uriBuilder.Query) + string.Format("&hmac_sign={0}",hmacRes);
}
}
}
}