Skip to content

Commit 497f8c4

Browse files
author
Leo
committed
Compiles, ready for test
1 parent 60f5a2b commit 497f8c4

File tree

2 files changed

+291
-0
lines changed

2 files changed

+291
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.IO;
5+
using System.Net.Http;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using FirebaseAdmin;
9+
using FirebaseAdmin.Messaging;
10+
using Google.Apis.Auth.OAuth2;
11+
using Google.Apis.Http;
12+
using Google.Apis.Json;
13+
using Google.Apis.Util;
14+
using Newtonsoft.Json;
15+
using Newtonsoft.Json.Linq;
16+
17+
/// <summary>
18+
/// A helper class for interacting with the Firebase Instance ID service.Implements the FCM
19+
/// topic management functionality.
20+
/// </summary>
21+
public sealed class InstanceIdClient
22+
{
23+
private readonly string iidHost = "https://iid.googleapis.com";
24+
25+
private readonly string iidSubscriberPath = "iid/v1:batchAdd";
26+
27+
private readonly string iidUnsubscribePath = "iid/v1:batchRemove";
28+
29+
private readonly ConfigurableHttpClient httpClient;
30+
31+
private readonly HttpErrorHandler errorHandler;
32+
33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="InstanceIdClient"/> class.
35+
/// </summary>
36+
/// <param name="clientFactory">A default implentation of the HTTP client factory.</param>
37+
/// <param name="credential">A GoogleCredential.</param>
38+
/// <param name="projectId">The Project Id for FCM Messaging.</param>
39+
public InstanceIdClient(HttpClientFactory clientFactory, GoogleCredential credential, string projectId)
40+
{
41+
if (string.IsNullOrEmpty(projectId))
42+
{
43+
throw new ArgumentException(
44+
"Project ID is required to access messaging service. Use a service account "
45+
+ "credential or set the project ID explicitly via AppOptions. Alternatively "
46+
+ "you can set the project ID via the GOOGLE_CLOUD_PROJECT environment "
47+
+ "variable.");
48+
}
49+
50+
this.httpClient = clientFactory.ThrowIfNull(nameof(clientFactory))
51+
.CreateAuthorizedHttpClient(credential);
52+
53+
this.errorHandler = new MessagingErrorHandler();
54+
}
55+
56+
/// <summary>
57+
/// Index of the registration token to which this error is related to.
58+
/// </summary>
59+
/// <param name="topic">The topic name to subscribe to.</param>
60+
/// <param name="registrationTokens">A list of registration tokens to subscribe.</param>
61+
/// <returns>The response produced by FCM topic management operations.</returns>
62+
public async Task<TopicManagementResponse> SubscribeToTopic(string topic, List<string> registrationTokens)
63+
{
64+
try
65+
{
66+
return await this.SendInstanceIdRequest(topic, registrationTokens, this.iidSubscriberPath).ConfigureAwait(false);
67+
}
68+
catch (HttpRequestException e)
69+
{
70+
throw this.CreateExceptionFromResponse(e);
71+
}
72+
catch (IOException)
73+
{
74+
throw new FirebaseMessagingException(ErrorCode.Internal, "Error while calling IID backend service");
75+
}
76+
}
77+
78+
/// <summary>
79+
/// Index of the registration token to which this error is related to.
80+
/// </summary>
81+
/// <param name="topic">The topic name to unsubscribe from.</param>
82+
/// <param name="registrationTokens">A list of registration tokens to unsubscribe.</param>
83+
/// <returns>The response produced by FCM topic management operations.</returns>
84+
public async Task<TopicManagementResponse> UnsubscribeFromTopic(string topic, List<string> registrationTokens)
85+
{
86+
try
87+
{
88+
return await this.SendInstanceIdRequest(topic, registrationTokens, this.iidUnsubscribePath).ConfigureAwait(false);
89+
}
90+
catch (HttpRequestException e)
91+
{
92+
throw this.CreateExceptionFromResponse(e);
93+
}
94+
catch (IOException)
95+
{
96+
throw new FirebaseMessagingException(ErrorCode.Internal, "Error while calling IID backend service");
97+
}
98+
}
99+
100+
private async Task<TopicManagementResponse> SendInstanceIdRequest(string topic, List<string> registrationTokens, string path)
101+
{
102+
string url = string.Format("%s/%s", this.iidHost, path);
103+
var body = new InstanceIdServiceRequest
104+
{
105+
Topic = this.GetPrefixedTopic(topic),
106+
RegistrationTokens = registrationTokens,
107+
};
108+
109+
var request = new HttpRequestMessage()
110+
{
111+
Method = HttpMethod.Post,
112+
RequestUri = new Uri(url),
113+
Content = NewtonsoftJsonSerializer.Instance.CreateJsonHttpContent(body),
114+
};
115+
116+
request.Headers.Add("access_token_auth", "true");
117+
118+
try
119+
{
120+
var response = await this.httpClient.SendAsync(request, default(CancellationToken)).ConfigureAwait(false);
121+
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
122+
this.errorHandler.ThrowIfError(response, json);
123+
return JsonConvert.DeserializeObject<TopicManagementResponse>(json);
124+
}
125+
catch (HttpRequestException e)
126+
{
127+
throw this.CreateExceptionFromResponse(e);
128+
}
129+
}
130+
131+
private FirebaseMessagingException CreateExceptionFromResponse(HttpRequestException e)
132+
{
133+
var temp = e.ToFirebaseException();
134+
return new FirebaseMessagingException(
135+
temp.ErrorCode,
136+
temp.Message,
137+
inner: temp.InnerException,
138+
response: temp.HttpResponse);
139+
}
140+
141+
private string GetPrefixedTopic(string topic)
142+
{
143+
if (topic.StartsWith("/topics/"))
144+
{
145+
return topic;
146+
}
147+
else
148+
{
149+
return "/topics/" + topic;
150+
}
151+
}
152+
153+
private class InstanceIdServiceRequest
154+
{
155+
[JsonProperty("to")]
156+
public string Topic { get; set; }
157+
158+
[JsonProperty("registration_tokens")]
159+
public List<string> RegistrationTokens { get; set; }
160+
}
161+
162+
private class InstanceIdServiceErrorResponse
163+
{
164+
[JsonProperty("error")]
165+
public string Error { get; set; }
166+
}
167+
168+
private class InstanceIdServiceResponse
169+
{
170+
[JsonProperty("results")]
171+
public List<JObject> Results { get; set; }
172+
}
173+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Newtonsoft.Json.Linq;
4+
5+
/// <summary>
6+
/// The response produced by FCM topic management operations.
7+
/// </summary>
8+
public class TopicManagementResponse
9+
{
10+
private readonly int successCount;
11+
private IReadOnlyList<Error> errors;
12+
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="TopicManagementResponse"/> class.
15+
/// </summary>
16+
/// <param name="results">The results from the response produced by FCM topic management operations.</param>
17+
public TopicManagementResponse(List<JObject> results)
18+
{
19+
if (results == null || results.Count == 0)
20+
{
21+
throw new ArgumentException("unexpected response from topic management service");
22+
}
23+
24+
var resultErrors = new List<Error>();
25+
for (var i = 0; i < results.Count; i++)
26+
{
27+
if (results[i].HasValues)
28+
{
29+
resultErrors.Add(new Error(i, results[i].Value<string>("error")));
30+
}
31+
else
32+
{
33+
this.successCount++;
34+
}
35+
}
36+
37+
this.errors = resultErrors;
38+
}
39+
40+
/// <summary>
41+
/// Gets the number of registration tokens that were successfully subscribed or unsubscribed.
42+
/// </summary>
43+
/// <returns>The number of registration tokens that were successfully subscribed or unsubscribed.</returns>
44+
public int GetSuccessCount()
45+
{
46+
return this.successCount;
47+
}
48+
49+
/// <summary>
50+
/// Gets the number of registration tokens that could not be subscribed or unsubscribed, and resulted in an error.
51+
/// </summary>
52+
/// <returns>The number of failures.</returns>
53+
public int GetFailureCount()
54+
{
55+
return this.errors.Count;
56+
}
57+
58+
/// <summary>
59+
/// Gets a list of errors encountered while executing the topic management operation.
60+
/// </summary>
61+
/// <returns>A non-null list.</returns>
62+
public IReadOnlyList<Error> GetErrors()
63+
{
64+
return this.errors;
65+
}
66+
67+
/// <summary>
68+
/// A topic management error.
69+
/// </summary>
70+
public class Error
71+
{
72+
// Server error codes as defined in https://developers.google.com/instance-id/reference/server
73+
// TODO: Should we handle other error codes here (e.g. PERMISSION_DENIED)?
74+
private readonly IReadOnlyDictionary<string, string> errorCodes;
75+
private readonly string unknownError = "unknown-error";
76+
77+
private int index;
78+
private string reason;
79+
80+
/// <summary>
81+
/// Initializes a new instance of the <see cref="Error"/> class.
82+
/// </summary>
83+
/// <param name="index">Index of the error in the error codes.</param>
84+
/// <param name="reason">Reason for the error.</param>
85+
public Error(int index, string reason)
86+
{
87+
this.errorCodes = new Dictionary<string, string>
88+
{
89+
{ "INVALID_ARGUMENT", "invalid-argument" },
90+
{ "NOT_FOUND", "registration-token-not-registered" },
91+
{ "INTERNAL", "internal-error" },
92+
{ "TOO_MANY_TOPICS", "too-many-topics" },
93+
};
94+
95+
this.index = index;
96+
this.reason = this.errorCodes.ContainsKey(reason)
97+
? this.errorCodes[reason] : this.unknownError;
98+
}
99+
100+
/// <summary>
101+
/// Index of the registration token to which this error is related to.
102+
/// </summary>
103+
/// <returns>An index into the original registration token list.</returns>
104+
public int GetIndex()
105+
{
106+
return this.index;
107+
}
108+
109+
/// <summary>
110+
/// String describing the nature of the error.
111+
/// </summary>
112+
/// <returns>A non-null, non-empty error message.</returns>
113+
public string GetReason()
114+
{
115+
return this.reason;
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)