-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnewsObject.js
More file actions
440 lines (388 loc) · 20.8 KB
/
newsObject.js
File metadata and controls
440 lines (388 loc) · 20.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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
// ********************************************************************************************************
// NewsWidget.js
// The NewsWidget module is an Angular JS-based client application utilizing streaming services provided
// by Elektron RealTime (ERT) to request and retrieve Machine Readable News (MRN) headlines and stories.
// The interface provides the ability to connect to the streaming services via the TREP (ADS) local
// installation or via the ERT (Elektron Real Time) in the Cloud.
//
// The widget provides a interface that displays the real-time headlines as they arrive offering the
// ability to display any associated news story if available. In addition, the interface also filters out
// those headlines specific to a specific set of codes (RICs) associated with the story.
//
// Note: When requesting for streaming services from RDP/ERT, applications must be authenticated using
// the HTTP RDP authentication services prior to connecting into the ERT services over WebSockets.
// To adhere to the "Same Origin" security policies, a simple server-side application (provided)
// will act as an application proxy managing RDP authentication. Refer to the instructions for setup.
//
// Authors: Nick Zincone
// Version: 2.0.1
// Date: June 2020.
// ********************************************************************************************************
// App
// Main Application entry point. Perform app-specific intialization within our closure
(function()
{
// Main application module. This application depends on the Angular 'ngAnimate' module.
// As the name implies, 'ngAnimate' provides animation using CSS styles which allows visual
// feedback when the status of the service is updated.
var app = angular.module('NewsWidget',['ngAnimate']);
// Application session configuration
// Define the session (TREP, RDP/ERT) you wish to use to access streaming services. To define your session,
// update the following setting:
// session: undefined
//
// Eg: session: 'RDP' // RDP/ERT Session
// session: 'ADS' // TREP/ADS Session
app.constant('config', {
session: 'RDP', // 'ADS' or 'RDP'.
// TREP (ADS) session.
// This section defines the connection and authentication requirements to connect directly into the
// streaming services from your locally installed TREP installation.
// Load this example directly within your browswer.
adsSession: {
wsServer: 'ewa', // Address of our ADS Elektron WebSocket server. Eg: 'elektron'
wsPort: '15000', // Address port of our ADS Elektron Websccket server. Eg: 15000
wsLogin: { // Elektron WebSocket login credentials
user: 'user', // User name. Optional. Default: desktop login.
appId: '256', // AppID. Optional. Default: '256'
position: '127.0.0.1' // Position. Optional. Default: '127.0.0.1'
}
},
// ERT (Elektron Real Time) in Cloud session.
// This section defines authenticastion to access RDP (Refinitiv Data Platform)/ERT.
// Start the local HTTP server (provided) and within your browser, specify the URL: http://localhost:8080/quoteObject.html
rdpSession: {
wsLogin: {
user: '',
password: '',
clientId: undefined
},
restAuthHostName: 'https://api.refinitiv.com/auth/oauth2/v1/token',
restServiceDiscovery: 'https://api.refinitiv.com/streaming/pricing/v1/',
wsLocation: 'us-east-1a'
},
//wsService: 'ELEKTRON_EDGE' // Optional. Elektron WebSocket service hosting realtime market data
});
// ****************************************************************
// Custom filters used when displaying data in our widget
// ****************************************************************
// ricList
// Extract the RICs from the subject array and format as a simple list of rics, space separated.
app.filter('ricList', function() {
return( function(rics) {
// Filter out the "R:" portion for each entry
var result = rics.map(ric => ric.substr(2));
if (result.length > 0) {
var list = "";
for (var i = 0; i < result.length; i++)
list = list + result[i] + " ";
return(list);
}
return("");
});
});
//******************************************************************************************
// Sharable Services
//
// widgetStatus - Capture the status messages generated from the Elektron WebSocket server
// and display as a pull-down list to see history.
//******************************************************************************************
app.factory('widgetStatus', $timeout => {
let statusList = [];
return ({
list: function () { return (statusList); },
update: function (txt,msg) {
(msg != null ? console.log(txt,msg) : console.log(txt));
let status = statusList[0];
if (!status || status.msg != txt) {
if (status)
statusList[0].id = 1;
// Force the callback to always run asynchronously - prevents error:inprog (Already in Progress) error
$timeout(() => {statusList.unshift({ id: 0, msg: txt })}, 0);
}
}
});
});
//***********************************
// Translation code
// added by James Sullivan 2019/09/19
// Needs a paid API key from the Google API Console: https://console.developers.google.com/apis/credentials
// Unfortunately free and trial keys are too slow to keep up with speed of news
// NOTE NOT SECURE this key is exposed on the client so only use for internal demo purposes
//
const apiKey = '';
window.onload = function() {
if(apiKey.length > 0) {
document.getElementById("langSelect").style.visibility = 'visible';
document.getElementById("langLabel").style.visibility = 'visible';
}
}
var storyTranslationObj = {
sourceLang: 'en',
targetLang: 'ja',
textToTranslate: 'This is a news story.',
format: "text"
};
var headlineTranslationObj = {
sourceLang: 'en',
targetLang: 'ja',
textToTranslate: 'This is a news headline.',
format: "text"
};
function translationURL(data) {
var url = "https://www.googleapis.com/language/translate/v2/" +
"?key=" + apiKey +
"&q=" + encodeURI(data.textToTranslate) +
"&target=" + data.targetLang +
"&source=" + data.sourceLang +
"&format=" + data.format;
return url
}
var noStoryData = new Map ([
['en', 'No story available'], // default
['ko', '뉴스 기사가 없습니다'],
['pt', 'Nenhuma notícia disponível'],
['es', 'No hay noticias disponibles'],
['zh', '没有可用的新闻报导'],
['ja', '利用可能なニュース記事はありません'],
]);
var noStory = new Proxy(noStoryData, {
get: function(target, id) {
return target.has(id) ? target.get(id) : target.get("en");
},
});
//**********************************************************************************************
// User-defined Directives
//
// animateOnChange - Directive to show change in our view.
//**********************************************************************************************
app.directive('animateOnChange', $animate => {
return ((scope, elem, attr) => {
scope.$watch(attr.animateOnChange, (newVal, oldVal) => {
if (newVal != oldVal) {
$animate.enter(elem, elem.parent(), elem, () => $animate.leave(elem));
}
})
});
});
// Widget Controller
// This controller manages all interaction, behavior and display within our application.
app.controller('widgetController', function ($scope, $rootScope, widgetStatus, config )
{
// Some initialization
$scope.statusList = widgetStatus.list();
this.filter = "";
this.selectedFilter = "";
this.alreadySelectedStory = null;
this.selectedStory = null;
this.selectedLanguage = ""
this.needsConfiguration = (config.session === undefined);
// *****************************************************************
// For simplicity, we capture all the MRN stories, in memory, as
// they stream to us.
// *****************************************************************
this.stories = []; // Our data model. Collection of ews stories.
// Define the WebSocket interface to manage our streaming services
this.ertController = new ERTWebSocketController();
// RDP Authentication
// Only applicable if a user chooses an ERT Session.
this.rdpController = new ERTRESTController();
// Initialize our session
switch (config.session) {
case 'ADS':
widgetStatus.update("Connecting to the WebSocket streaming service on ["+ config.adsSession.wsServer + ":" + config.adsSession.wsPort + "]");
this.ertController.connectADS(config.adsSession.wsServer, config.adsSession.wsPort, config.adsSession.wsLogin.user,
config.adsSession.wsLogin.appId, config.adsSession.wsLogin.position);
break;
case 'RDP':
widgetStatus.update("Authenticating with RDP using " + config.rdpSession.restAuthHostName + "...");
this.rdpController.get_access_token({
'username': config.rdpSession.wsLogin.user,
'password': config.rdpSession.wsLogin.password,
'clientId': config.rdpSession.wsLogin.clientId,
'location': config.rdpSession.wsLocation
});
break;
}
this.selectStory = function(story) {
if(story != null && story.body.length > 0 &&
!(this.selectedLanguage == '' || apiKey == '' || story.language == this.selectedLanguage)){
storyTranslationObj.textToTranslate = story.body.replace('\r\n', '<br>');
storyTranslationObj.targetLang = this.selectedLanguage;
storyTranslationObj.sourceLang = story.language;
this.makeStoryTranslationRequest(storyTranslationObj, story)
}
this.selectedStory = story;
}
this.alreadySelectedStory = function(){
if(this.selectedStory != null) { this.selectStory(this.selectedStory) } else{null;}
}
this.makeStoryTranslationRequest = function(data, mySelectedStory) {
var obj = {key: apiKey, source: data.sourceLang, target: data.targetLang, q: data.textToTranslate.replace(/\n/gm, '(yyyyyy)'), format: data.format}
let json = JSON.stringify(obj);
var s = new XMLHttpRequest();
s.open("POST", "https://www.googleapis.com/language/translate/v2?key=" + apiKey, true);
//Send the proper header information along with the request
s.setRequestHeader("Content-Type", "application/json; charset=utf-8");
s.setRequestHeader("Accept", "application/json");
s.onloadend = function() {
if (s.status == 200) {
var translation = JSON.parse(s.responseText).data.translations[0].translatedText;
// Google translate destroys line breaks so ugly hacks are necessary
mySelectedStory.translatedBody = translation.replace(/[(((]\s?([yY]{5,9}|[a]{5,9})[)))]/gm, '\r\n').replace(/<br>/gm, '\r\n');
mySelectedStory.translatedLanguage = data.targetLang
} else {
console.log("story translation error " + this.status);
}
}
s.send(json);
}
this.selectedStoryText = function() {
if(this.selectedStory != null && this.selectedStory.body.length > 0 &&
(this.selectedLanguage == '' || apiKey == '' || this.selectedStory.language == this.selectedLanguage)){
return(this.selectedStory.body)
} else if (this.selectedStory != null && this.selectedStory.body.length > 0 && (this.selectedStory.translatedLanguage == null || this.selectedStory.translatedLanguage != this.selectedLanguage)) {
return("....")
} else if (this.selectedStory != null && this.selectedStory.body.length > 0 && this.selectedStory.translatedLanguage == this.selectedLanguage) {
return(this.selectedStory.translatedBody)
} else {
return(noStory[this.selectedLanguage]);
}
}
//***********************************************************************************
// ERTRESTController.onStatus
//
// Capture all ERTRESTController status messages.
// RDP/ERT uses OAuth 2.0 authentication and requires clients to use access tokens to
// retrieve streaming content. In addition, RDP/ERT requires clients to continuously
// refresh the access token to continue uninterrupted service.
//
// The following callback will capture the events related to retrieving and
// continuously updating the tokens in order to provide the streaming interface these
// details to maintain uninterrupted service.
//***********************************************************************************
this.rdpController.onStatus((eventCode, msg) => {
let status = this.rdpController.status;
switch (eventCode) {
case status.getRefreshToken: // Get Access token form RDP (re-refresh Token case)
this.auth_obj = msg;
widgetStatus.update("RDP Authentication Refresh success. Refreshing ERT stream...");
this.ertController.refreshERT(msg);
break;
case status.getService: // Get Service Discovery information form RDP
// Connect into ERT in Cloud Elektron WebSocket server
this.ertController.connectERT(msg.hostList, msg.portList, msg.access_token, config.rdpSession.appId, config.rdpSession.position);
break;
case status.authenError: // Get Authentication fail error form RDP
widgetStatus.update("Elektron Real Time in Cloud authentication failed. See console.", msg);
break;
case status.getServiceError: // Get Service Discovery fail error form RDP
widgetStatus.update("Elektron Real Time in Cloud Service Discovery failed. See console.", msg);
break;
}
});
//*******************************************************************************
// ERTWebSocketController.onStatus
//
// Capture all ERTWebSocketController status messages.
//*******************************************************************************
this.ertController.onStatus( (eventCode, msg) => {
let status = this.ertController.status;
switch (eventCode) {
case status.connected:
// ERTWebSocketController first reports success then will automatically
// attempt to log in to the ERT WebSocket server...
console.log(`Successfully connected into the ERT WebSocket server: ${msg.server}:${msg.port}`);
break;
case status.disconnected:
widgetStatus.update(`Connection to ERT streaming server: ${msg.server}:${msg.port} is down/unavailable`);
break;
case status.loginResponse:
this.processLogin(msg);
break;
case status.msgStatus:
// Report potential issues with our requested market data item
this.error = (msg.Key ? msg.Key.Name+":" : "");
this.error += msg.State.Text;
widgetStatus.update("Status response for item: " + this.error);
break;
case status.msgError:
// Report invalid usage errors
widgetStatus.update(`Invalid usage: ${msg.Text}. ${msg.Debug.Message}`);
break;
case status.tokenExpire:
widgetStatus.update("Elektron Data Platform Authentication Expired");
break;
case status.refreshSuccess:
widgetStatus.update("Elektron Data Platform Authentication Refresh success")
break;
case status.processingError:
// Report any general controller issues
widgetStatus.update(msg);
break;
}
});
//*********************************************************************************
// processLogin
// Determine if we have successfully logged into our WebSocket server. Within
// our Login response, we need to check the following stanza:
//
// "State": {
// "Stream": <stream state>, "Open" | "Closed"
// "Data": <data state>, "Ok" | "Suspect"
// "Text": <reason>
// }
//
// If we logged in, open the news stream.
//*********************************************************************************
this.processLogin = function (msg) {
widgetStatus.update("Login state: " + msg.State.Stream + "/" + msg.State.Data + "/" + msg.State.Text);
if (this.ertController.loggedIn()) {
widgetStatus.update("Requesting news headlines and stories...");
this.ertController.requestNews("MRN_STORY", config.wsService);
}
};
//********************************************************************************************
// TRWebSocketController.onNews
// Capture all news stories generated from MRN. All stories presented here are complete and
// decompressed. We simply store each story within our data model.
//********************************************************************************************
this.ertController.onNews( (ric, story) => {
$scope.$apply( () => {
// Abstract API request function
function makeHeadlineTranslationRequest(data, storiesPointer) {
var url = translationURL(data)
var r = new XMLHttpRequest();
r.open("GET", url, true);
//Send the proper header information along with the request
r.setRequestHeader("Content-Type", "application/json");
r.setRequestHeader("Accept", "application/json");
r.onreadystatechange = function () {
if (r.readyState != 4 || r.status != 200) return;
var translation = JSON.parse(r.responseText).data.translations[0].translatedText;
story.translatedHeadline = translation;
storiesPointer.unshift(story);
};
return r.send();
}
// Store the new story
if(story.headline.length > 0) {
if(this.selectedLanguage == '' || apiKey == '' || story.language == this.selectedLanguage) {
// 'no need to translate'
story.translatedHeadline = story.headline;
this.stories.unshift(story);
} else {
// 'translating from ' + story.language + ' to ' + this.selectedLanguage
headlineTranslationObj.textToTranslate = story.headline;
headlineTranslationObj.targetLang = this.selectedLanguage;
headlineTranslationObj.sourceLang = story.language;
makeHeadlineTranslationRequest(headlineTranslationObj, this.stories);
}
}
// Simple trim to keep the stories in memory manageable
if ( this.stories.length > 1000 )
this.stories.pop();
});
});
});
})();