Skip to content

Commit cc9ce65

Browse files
Implemented support for sending messages over SNS + Raw message delivery
1 parent 6bc231d commit cc9ce65

File tree

9 files changed

+249
-72
lines changed

9 files changed

+249
-72
lines changed

GuiStack/Controllers/SNS/SubscriptionsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public async Task<ActionResult> CreateSubscription([FromBody] SNSCreateSubscript
6666

6767
try
6868
{
69-
await snsRepository.CreateTopicSubscriptionAsync(model.TopicArn, model.Endpoint);
69+
await snsRepository.CreateTopicSubscriptionAsync(model);
7070
return Ok();
7171
}
7272
catch(Exception ex)

GuiStack/Controllers/SNS/TopicsController.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
using System;
1111
using System.Net;
12+
using System.Text;
1213
using System.Threading.Tasks;
1314
using Amazon.SimpleNotificationService;
1415
using GuiStack.Extensions;
@@ -83,5 +84,46 @@ public async Task<ActionResult> DeleteTopic([FromRoute] string topicArn)
8384
return HandleException(ex);
8485
}
8586
}
87+
88+
[HttpPost("{topicArn}")]
89+
[Consumes("application/json")]
90+
[Produces("application/json")]
91+
public async Task<ActionResult> SendMessage([FromRoute] string topicArn, [FromBody] SQSSendMessageModel message)
92+
{
93+
if(message == null)
94+
return StatusCode((int)HttpStatusCode.BadRequest);
95+
96+
try
97+
{
98+
string body = message.Body;
99+
100+
if(message.IsProtobuf)
101+
{
102+
byte[] protoData = Convert.FromBase64String(body);
103+
104+
// This re-encoding to Base64 is intentional, as I want to rely on .NET's Base64 implementation to ensure that
105+
// in the event that encoding isn't properly performed by the client, we don't send incorrectly encoded messages.
106+
// (Instead, the decoding above will likely throw an error)
107+
108+
if(message.Base64Encode)
109+
body = Convert.ToBase64String(protoData);
110+
else
111+
body = protoData.ToRawString();
112+
}
113+
else if(message.Base64Encode)
114+
{
115+
byte[] data = Encoding.UTF8.GetBytes(body);
116+
body = Convert.ToBase64String(data);
117+
}
118+
119+
var messageId = await snsRepository.SendMessageAsync(topicArn, body);
120+
121+
return Json(new { messageId = messageId });
122+
}
123+
catch(Exception ex)
124+
{
125+
return HandleException(ex);
126+
}
127+
}
86128
}
87129
}

GuiStack/Models/SNSCreateSubscriptionModel.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ public class SNSCreateSubscriptionModel
1616
public string TopicArn { get; set; }
1717
public string Protocol { get; set; }
1818
public string Endpoint { get; set; }
19+
public bool RawMessageDelivery { get; set; }
1920
}
2021
}

GuiStack/Models/SNSSubscription.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,20 @@ public class SNSSubscription
1919
public string Protocol { get; set; }
2020
public string Endpoint { get; set; }
2121
public string Owner { get; set; }
22+
public bool RawMessageDelivery { get; set; }
2223

2324
public SNSSubscription()
2425
{
2526
}
2627

27-
public SNSSubscription(string arn, string topicArn, string protocol, string endpoint, string owner)
28+
public SNSSubscription(string arn, string topicArn, string protocol, string endpoint, string owner, bool rawMessageDelivery)
2829
{
2930
Arn = Arn.Parse(arn);
3031
TopicARN = Arn.Parse(topicArn);
3132
Protocol = protocol;
3233
Endpoint = endpoint;
3334
Owner = owner;
35+
RawMessageDelivery = rawMessageDelivery;
3436
}
3537
}
3638
}

GuiStack/Pages/SNS/Index.cshtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ else
3636
<input type="text" class="name-textbox text-center" maxlength="80" style="width: 400px" />
3737
</p>
3838
<p>
39-
<input type="checkbox" id="new-sns-topic-fifo-checkbox" class="fifo-checkbox" />
39+
<input type="checkbox" id="new-sns-topic-fifo-checkbox" />
4040
<label for="new-sns-topic-fifo-checkbox">FIFO</label>
4141
</p>
4242
<p id="new-sns-topic-dedupe-checkbox-container" style="display: none">
43-
<input type="checkbox" id="new-sns-topic-dedupe-checkbox" class="fifo-checkbox" />
43+
<input type="checkbox" id="new-sns-topic-dedupe-checkbox" />
4444
<label for="new-sns-topic-dedupe-checkbox">Content-based deduplication</label>
4545
</p>
4646

GuiStack/Pages/SNS/_TopicInfo.cshtml

Lines changed: 146 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
<div class="queue-selector-container text-left" style="margin-top: 16px; margin-bottom: 16px">
2121
Loading queues...
2222
</div>
23+
24+
<p>
25+
<input type="checkbox" id="sns-subscribe-to-sqs-raw-delivery-checkbox" />
26+
<label for="sns-subscribe-to-sqs-raw-delivery-checkbox">Raw message delivery</label>
27+
</p>
2328

2429
<div class="modal-buttons text-center">
2530
<button onclick="sns_CreateSubscription_Click()">Create subscription</button>
@@ -77,82 +82,102 @@
7782
</tbody>
7883
</table>
7984

80-
<div style="display: flex; align-items: center">
81-
<h2>Subscriptions</h2>
82-
<div style="text-align: right; font-size: 1.5em; flex-grow: 1">
83-
<a no-href onclick="showWindow('sns-subscribe-to-sqs-modal')" class="gs-icon-stack initial-white neon-green">
84-
<i class="fa-solid fa-list-check" style="margin-right: 4px"></i>
85-
<i class="bi bi-plus-circle-fill gs-icon-overlay stroked" style="color: #000000"></i>
86-
</a>
85+
<div class="gs-tab-control">
86+
<div class="gs-tab-container">
87+
<div class="gs-tabitem selected" data-tabpage="sns-send">Send</div>
88+
<div class="gs-tabitem" data-tabpage="sns-subscriptions">Subscriptions</div>
8789
</div>
88-
</div>
8990

90-
<table class="gs-list padded autosize-all-cols-but-first">
91-
<thead>
92-
<tr>
93-
<th>Endpoint</th>
94-
<th>Protocol</th>
95-
<th>Owner</th>
96-
<th>Actions</th>
97-
</tr>
98-
</thead>
99-
<tbody>
100-
@foreach(var subscription in Model.Subscriptions.OrderBy(s => s.Protocol).ThenBy(s => s.Endpoint))
91+
<div id="sns-send" class="gs-tabpage selected">
92+
@{ await Html.RenderPartialAsync("~/Pages/SQS/_MessageEditorPartial.cshtml", new SQSMessageEditorModel("sns-send-message-editor", "__snsSendMessageEditor", "__snsSendMessageTextEditor")); }
93+
</div>
94+
95+
<div id="sns-subscriptions" class="gs-tabpage">
96+
<div style="display: flex; align-items: center">
97+
<h2>Subscriptions</h2>
98+
<div style="text-align: right; font-size: 1.5em; flex-grow: 1">
99+
<a no-href onclick="showWindow('sns-subscribe-to-sqs-modal')" class="gs-icon-stack initial-white neon-green">
100+
<i class="fa-solid fa-list-check" style="margin-right: 4px"></i>
101+
<i class="bi bi-plus-circle-fill gs-icon-overlay stroked" style="color: #000000"></i>
102+
</a>
103+
</div>
104+
</div>
105+
106+
@if(Model.Subscriptions != null && Model.Subscriptions.Any())
101107
{
102-
<tr data-arn="@subscription.Arn" data-endpoint="@subscription.Endpoint">
103-
@if(subscription.Protocol == null)
104-
{
105-
<td>@subscription.Endpoint</td>
106-
}
107-
else
108-
{
109-
<td>
110-
@switch(subscription.Protocol.ToLower())
111-
{
112-
case "sqs":
113-
{
114-
if (Arn.TryParse(subscription.Endpoint, out var arn))
108+
<table class="gs-list padded autosize-all-cols-but-first">
109+
<thead>
110+
<tr>
111+
<th>Endpoint</th>
112+
<th>Protocol</th>
113+
<th>Raw Delivery</th>
114+
<th>Owner</th>
115+
<th>Actions</th>
116+
</tr>
117+
</thead>
118+
<tbody>
119+
@foreach(var subscription in Model.Subscriptions.OrderBy(s => s.Protocol).ThenBy(s => s.Endpoint))
120+
{
121+
<tr data-arn="@subscription.Arn" data-endpoint="@subscription.Endpoint">
122+
<td>
123+
@switch(subscription.Protocol?.ToLower() ?? "")
115124
{
116-
<a href="/SQS/@arn.Resource"><i class="fa-solid fa-database gs-rotate-270" style="margin-right: 2px"></i> @arn.Resource</a>
117-
}
118-
else
119-
{
120-
<td>@subscription.Endpoint</td>
125+
case "sqs":
126+
{
127+
if(!Arn.TryParse(subscription.Endpoint, out var arn))
128+
goto default;
129+
130+
<a href="/SQS/@arn.Resource"><i class="fa-solid fa-database gs-rotate-270" style="margin-right: 2px"></i> @arn.Resource</a>
131+
break;
132+
}
133+
134+
default:
135+
@subscription.Endpoint
136+
break;
121137
}
122-
break;
123-
}
124-
}
125-
</td>
126-
}
127-
<td>@subscription.Protocol</td>
128-
<td>@(!string.IsNullOrWhiteSpace(subscription.Owner) ? subscription.Owner : "(unknown)")</td>
129-
<td>
130-
<div class="gs-icons">
131-
<a no-href class="purple lnk-sns-sub-copyarn" title="Copy Endpoint address/ARN">
132-
<i class="fa-solid fa-link"></i>
133-
</a>
134-
<a no-href class="red lnk-sns-sub-delete" title="Delete">
135-
<i class="fa-solid fa-trash-can"></i>
136-
</a>
137-
</div>
138-
</td>
139-
</tr>
138+
</td>
139+
<td>@subscription.Protocol</td>
140+
<td>@(subscription.RawMessageDelivery ? "Yes" : "No")</td>
141+
<td>@(!string.IsNullOrWhiteSpace(subscription.Owner) ? subscription.Owner : "(unknown)")</td>
142+
<td>
143+
<div class="gs-icons">
144+
<a no-href class="purple lnk-sns-sub-copyarn" title="Copy Endpoint address/ARN">
145+
<i class="fa-solid fa-link"></i>
146+
</a>
147+
<a no-href class="red lnk-sns-sub-delete" title="Delete">
148+
<i class="fa-solid fa-trash-can"></i>
149+
</a>
150+
</div>
151+
</td>
152+
</tr>
153+
}
154+
</tbody>
155+
</table>
140156
}
141-
</tbody>
142-
</table>
157+
else
158+
{
159+
<text>No active subscriptions</text>
160+
}
161+
</div>
162+
</div>
143163

144164
<script type="text/javascript">
165+
const SNS_URL_DELETE_SUBSCRIPTION = "@Url.Action("DeleteSubscription", "Subscriptions", new { subscriptionArn = "__SUBSCRIPTIONARN__" })";
166+
167+
var prompt_SubscriptionArn;
168+
169+
var __snsSendLanguageSelect = __snsSendMessageEditor.querySelector(".lang-select");
170+
var __snsSendButton = __snsSendMessageEditor.querySelector(".lnk-sqs-send");
171+
145172
var subscribeToSqsModal = document.getElementById("sns-subscribe-to-sqs-modal");
146173
var queueSelectorContainer = $("#sns-subscribe-to-sqs-modal .queue-selector-container");
147174
175+
__snsSendButton.addEventListener("click", sns_SendMessage_Click);
176+
148177
$(".lnk-sns-sub-copyarn").click(sns_CopySubscriptionArn);
149178
$(".lnk-sns-sub-delete").click(sns_DeleteSubscriptionPrompt);
150179
$("#delete-sns-subscription-modal .yes-button").click(sns_DeleteSubscription);
151180
152-
const SNS_URL_DELETE_SUBSCRIPTION = "@Url.Action("DeleteSubscription", "Subscriptions", new { subscriptionArn = "__SUBSCRIPTIONARN__" })";
153-
154-
var prompt_SubscriptionArn;
155-
156181
subscribeToSqsModal.addEventListener("windowopened", function() {
157182
$.ajax({
158183
type: "GET",
@@ -172,6 +197,63 @@
172197
queueSelectorContainer.html("Loading queues...");
173198
});
174199
200+
function sns_SendMessage_Click(event)
201+
{
202+
var lang = __snsSendLanguageSelect.value;
203+
var data = __snsSendMessageTextEditor.getValue();
204+
var isProtobuf = (lang == "protojson");
205+
var base64Encode = __snsSendMessageEditor.querySelector(".sqs-send-as-base64").checked;
206+
207+
if(isProtobuf)
208+
{
209+
try
210+
{
211+
var protoPath = __snsSendMessageEditor.getAttribute("data-selected-proto");
212+
213+
if(isNull(protoPath) || protoPath.length <= 0)
214+
{
215+
gs_DisplayError("No Protobuf definition selected");
216+
return;
217+
}
218+
219+
var protoRoot = gs_GetProtobufRootDefinition(protoPath);
220+
var protoType = protoPath
221+
.replace(/^proto\/[^\/]+\/?/, "")
222+
.replace("/", ".");
223+
224+
if(protoType.length <= 0)
225+
throw "'" + protoPath + "' is not a valid protobuf definition path: Path is too short";
226+
227+
var root = protobuf.Root.fromJSON(JSON.parse(protoRoot));
228+
var type = root.lookupType(protoType);
229+
var payload = JSON.parse(data);
230+
var message = type.fromObject(payload);
231+
232+
data = gs_Uint8ArrayToBase64(type.encode(message).finish());
233+
}
234+
catch(error)
235+
{
236+
gs_DisplayError("An error occurred while constructing the Protobuf message: " + error);
237+
return;
238+
}
239+
}
240+
241+
$.ajax({
242+
type: "POST",
243+
url: "@Url.Action("SendMessage", "Topics", new { topicArn = Model.Topic.TopicARN.ToString() ?? "" })",
244+
contentType: "application/json",
245+
data: JSON.stringify({
246+
body: data,
247+
isProtobuf: isProtobuf,
248+
base64Encode: base64Encode
249+
}),
250+
error: gsevent_AjaxError,
251+
success: function(result) {
252+
__snsSendMessageTextEditor.setValue("");
253+
}
254+
});
255+
}
256+
175257
async function sns_CreateSubscription_Click()
176258
{
177259
var selectedItem = gs_GetSelectedTableItem(queueSelectorContainer[0]);
@@ -183,6 +265,7 @@
183265
}
184266
185267
var queueUrl = selectedItem.getAttribute("data-url");
268+
var rawMessageDelivery = document.getElementById("sns-subscribe-to-sqs-raw-delivery-checkbox").checked;
186269
187270
try
188271
{
@@ -192,7 +275,8 @@
192275
body: JSON.stringify({
193276
topicArn: "@Model.Topic.TopicARN",
194277
protocol: "sqs",
195-
endpoint: queueUrl
278+
endpoint: queueUrl,
279+
rawMessageDelivery: rawMessageDelivery
196280
})
197281
});
198282

GuiStack/Pages/SQS/Index.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ else
3535
<input type="text" class="name-textbox text-center" maxlength="80" style="width: 400px" />
3636
</p>
3737
<p>
38-
<input type="checkbox" id="new-sqs-queue-fifo-checkbox" class="fifo-checkbox" />
38+
<input type="checkbox" id="new-sqs-queue-fifo-checkbox" />
3939
<label for="new-sqs-queue-fifo-checkbox">FIFO</label>
4040
</p>
4141

0 commit comments

Comments
 (0)