Het lezen van topics in pub/sub kan op verschillende manieren, waarbij de pull de standaard is.
Deze laat je toe om onder eigen controle de berichten te raadplegen en hiermee aan de slag te gaan.
Om toegang te hebben tot de pub/sub dien je de OAUTH functies te gebruiken.
In de volgende voorbeelden gaan we tonen hoe je een publisch, subscribe en pull kunt uitvoeren.
Om dit te kunnen gebruiken dien je eveneens een user interface te voorzien, waarbij de privacy concent kan bevestigd worden.
Deze code die je uit te voeren vanuit een add-on menu, zoniet kan je toestemming niet verlenen aan je script.
Tevens moet je persoonlijke ID en Secret in je code zitten (of ingelezen worden), wat een extra veiligheidsrisico inhoud.
var CLIENT_ID = '394172514964-8ni4ej55ve1e-------.apps.googleusercontent.com';
var CLIENT_SECRET = 'GOCSPX-Yh6Y8n7zRgY----_VDAAdL';
// var CLIENT_ID = 'on4awm@myuba.be';
// var CLIENT_SECRET = 'myubaON4AWM';
function getPubSubService() {
return OAuth2.createService('SpreadsheetPubSub')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/v2/auth')
.setTokenUrl('https://oauth2.googleapis.com/token')
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
// .setCache(CacheService.getUserCache())
// .setLock(LockService.getUserLock())
.setScope(['https://www.googleapis.com/auth/pubsub','https://www.googleapis.com/auth/script.external_request'])
.setParam('access_type', 'offline')
.setParam('approval_prompt', 'force')
.setParam('login_hint', Session.getActiveUser().getEmail());
}
function authCallback(request) {
var service = getPubSubService();
var isAuthorized = service.handleCallback(request);
if (isAuthorized) {
closeSidebar(); // optional
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
// call this to re-test authorization
function reset() {
var service = getPubSubService();
service.reset();
}
function logRedirectUri() {
Logger.log(OAuth2.getRedirectUri());
}+
Bovenstaande code zorgt ervoor dat je de toegang verkrijgt via een token. Dit token is in dit voorbeeld opgegeven via de client ID en Secret(onveilig), maar kan ook zoals in de uitleg over de toegang tot de firestore.(document properties die één maal worden ingelezen en daardoor onzichtbaar worden in de code)
De service Scope hier vermeld laat je toe om met je eigen email account toegang te krijgen, indien je bent toegevoegd aan de GCP.
Vermits het authoriseren van je eigen account een manuele actie vereist dien je de code te draaien vanuit de sheet, zodat er een popup kan komen met de vraag om toegelaten te worden.(voeg toe aan add-on menu)
function onClick() {
publishPubSub('QuizToFirestore', 'Classroom_courseworks', 'data_pubsub', { 'score': '20' })
getListSubcriptions('QuizToFirestore', 'Classroom_courseworks')
getSubcription('projects/QuizToFirestore/subscriptions/Classroom_courseworks-sub')
}
function showSidebar(service) {
var authorizationUrl = service.getAuthorizationUrl();
var template = HtmlService.createTemplate(
'Click <a href="<?= authorizationUrl ?>" target="_blank">Authorize</a>. This sidebar will automatically closed when authorization is complete.');
template.authorizationUrl = authorizationUrl;
var page = template.evaluate();
SpreadsheetApp.getUi().showSidebar(page);
}
function closeSidebar() {
var html = HtmlService.createHtmlOutput("<script>google.script.host.close();</script>");
SpreadsheetApp.getUi().showSidebar(html);
}
De afhandeling hoe je ipv een persoonlijk account, een service account kan gebruiken, vind je in de code: Service Account Access.
Deze bied een aantal voordelen naar het beheer van toegang op GCP niveau, maar houden ook een risico in en vereisen een andere aanpak van toegang verlenen etc.
De functies laten je toe, indien je de juiste credentials hebt, om messages te publiceren, een lijst van de subscriptions te krijgen en de berichten te lezen die eventueel via een andere service (classroom) zijn toegekend.
Alle functies maken gebruik van de OAUTH afhandeling (REST API)
/**
* POST /v1/{topic}:publish
*/
function makePubSubRequest(service, project, topic, data, attr) {
var url = Utilities.formatString("https://pubsub.googleapis.com/v1/projects/%s/topics/%s:publish", project, topic);
var body = {
messages: [
{
attributes: attr,
data: Utilities.base64Encode(data)
}
]
};
var response = UrlFetchApp.fetch(url, {
method: "POST",
contentType: 'application/json',
muteHttpExceptions: true,
payload: JSON.stringify(body),
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
var message = JSON.stringify(result);
return {
log: message
}
}
De data voor een publish dient steeds base64 gecodeerd te zijn en wordt dmv een POST met attributen gepubliceert.
/**
* GET /v1/{topic}/subscriptions
*/
function getListSubcriptions(project, topic) {
var service = getPubSubService();
if (!service.hasAccess()) {
showSidebar(service)
}
else {
var url = Utilities.formatString("https://pubsub.googleapis.com/v1/projects/%s/topics/%s/subscriptions", project, topic);
// var body = {
// messages: [
// {
// attributes: attr,
// data: Utilities.base64Encode(data)
// }
// ]
// };
var response = UrlFetchApp.fetch(url, {
method: "GET",
contentType: 'application/json',
muteHttpExceptions: true,
// payload: JSON.stringify(body),
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
var message = JSON.stringify(result);
return {
log: message
}
}
}
Wil je weten welke subscriptions een bepaald topic hebben, dan kan je dit met bovenstaande functie opvragen.
/**
* POST https://pubsub.googleapis.com/v1/{subscription}:pull
* projects/QuizToFirestore/subscriptions/Classroom_courseworks-sub
* https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.subscriptions/pull
*
* {"collection":"courses.courseWork.studentSubmissions",
* "eventType":"MODIFIED",
* "resourceId":{
* "courseId":"141368853122",
* "courseWorkId":"141386889474",
* "id":"Cg4Iw4v20Y4EEILywtqOBA"
* }}
*/
function getSubcription(subscription) {
var service = getPubSubService();
if (!service.hasAccess()) {
showSidebar(service)
}
else {
var url = Utilities.formatString("https://pubsub.googleapis.com/v1/%s:pull", subscription,{enableExactlyOnceDelivery: true,messageOrderingEnabled: true});
var body = {
"maxMessages": 2,
};
var response = UrlFetchApp.fetch(url, {
method: "POST",
contentType: 'application/json',
muteHttpExceptions: true,
payload: JSON.stringify(body),
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
var obj = {}
var ackIds=[]
if (result.receivedMessages) {
for (a = 0; a < result.receivedMessages.length; a++) {
var ackId = result.receivedMessages[a].ackId
let messageId = result.receivedMessages[a].message.messageId
let messageIdpublishTime = result.receivedMessages[a].message.publishTime
let registrationId = result.receivedMessages[a].message.attributes.registrationId
// let message = result.receivedMessages[a].message
// bevat body van pub/sub bericht in base64 UTF-8
let data = result.receivedMessages[a].message.data
// converteer base64 naar byte code
let string = Utilities.base64DecodeWebSafe(data, Utilities.Charset.UTF_8)
Logger.log("string: " + Utilities.newBlob(string).getDataAsString()+ " messageId:"+ messageId +" Ptime:"+ messageIdpublishTime + " regId:" + registrationId)
//maak string van bytecode
let stringdata = Utilities.newBlob(string).getDataAsString()
//haal obj uit JSON string
try {
obj = JSON.parse(stringdata)
var message = JSON.stringify(obj);
// Logger.log("obj: " + obj)
// Logger.log("collection: " + obj.collection)
// Logger.log("eventType: " + obj.eventType)
// Logger.log("resourceId: " + obj.resourceId)
// Logger.log("courseId: " + obj.resourceId.courseId)
// Logger.log("courseWorkId: " + obj.resourceId.courseWorkId)
// Logger.log("id: " + obj.resourceId.id)
var courseId= obj.resourceId.courseId
var courseWorkId = obj.resourceId.courseWorkId
var resourceId = obj.resourceId.id
var score= Classroom.Courses.CourseWork.StudentSubmissions.get(courseId,courseWorkId,resourceId)
Logger.log(score.assignedGrade)
} catch (e) {
Logger.log("not an obj")
obj = {}
}
ackIds.push(ackId)
// ackPull(subscription, ackId)
}
ackPull(subscription,ackIds)
return {
log: message
}
} else {
Logger.log("no messages")
}
}
}
/**
* POST https://pubsub.googleapis.com/v1/projects/myproject/subscriptions/mysubscription:acknowledge
{
"ackIds": [
"dQNNHlAbEGEIBERNK0EPKVgUWQYyODM2LwgRHFEZDDsLRk1SK..." ]
}
*/
function ackPull(subscription, ackIds) {
var service = getPubSubService();
if (!service.hasAccess()) {
showSidebar(service)
}
else {
var url = Utilities.formatString("https://pubsub.googleapis.com/v1/%s:acknowledge", subscription);
var body = {
"ackIds": ackIds
};
var response = UrlFetchApp.fetch(url, {
method: "POST",
contentType: 'application/json',
muteHttpExceptions: true,
payload: JSON.stringify(body),
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
var message = JSON.stringify(result);
Logger.log("result: "+ message)
return {
log: message
}
}
}
Voor het lezen van de data dmv pull dien je mee te geven over hoeveel berichten je maximaal simultaan wil kunnen lezen (pull).
Tevens bepalen we dat de brichten slechts één maal en liefst in volgorde gelezen worden.
.....{enableExactlyOnceDelivery: true,messageOrderingEnabled: true}
Dit is geen absoluut gebeuren, daar het kan gebeuren dat door netwerkfouten etc, berichten of ack verloren gaan of elkaar kruisen, maar het beperkt wel de overhead.
De data die je terug krijgt bevat de ackId, nodig om het ontvangen van het bericht te bevestigen en de ge-encodeerde data.
Het is belangrijk dat de acknowledge gebeurt voor alle berichten die je ontvangen hebt dmv één POST.
Deze ackId's worden samen gebracht in een array en met één post aan de pub/sub service aangeboden.
Wanneer je client soft heel wat verwerkingstijd nodig heeft kan dit leiden tot het overschreiden van de max ack time. zie modifyAckDeadline
Het decoderen van de data, die bij een classroom bestaat uit een object structuur, vraagt voor een aantal stappen.
De data zit vervat in result.receivedMessages[a].message.data
Deze data dient base64 gedecodeerd te worden.
let string = Utilities.base64DecodeWebSafe(data,Utilities.Charset.UTF_8)
Vermits dit als resultaat een bytestring geeft, dienen we dit terug om te vormen tot een string, zodat we er als laatste decoding een JSON parse kunnen op los late om zo een JS object te genereren.
let stringdata =Utilities.newBlob(string).getDataAsString()
obj=JSON.parse(stringdata)
Hierna kun je nu alle data uit de structuur benaderen zoals binnen een JS object.
Deze data kan je dan gebruiken om de score of andere items te halen uit de structuur van de submission.
Logger.log("collection: " + obj.collection)
Logger.log("eventType: " + obj.eventType)
//object in object
Logger.log("resourceId: " + obj.resourceId)
Logger.log("courseId: " + obj.resourceId.courseId)
Logger.log("courseWorkId: " + obj.resourceId.courseWorkId)
Logger.log("id: " + obj.resourceId.id)
var courseId= obj.resourceId.courseId
var courseWorkId = obj.resourceId.courseWorkId
var resourceId = obj.resourceId.id
var score= Classroom.Courses.CourseWork.StudentSubmissions.get(courseId,courseWorkId,resourceId)
Logger.log(score.assignedGrade)
Als laatste dien je de ontvangen berichten te bevestigen in pub/sub.
Hiervoor heb je de ackId's nodig, die eveneens terug te vinden is in het bericht.
let ackId = result.receivedMessages[a].ackId