From f002532c1e5ac842e8e300a7fcaae689dd0be596 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 26 May 2022 19:09:46 -0500 Subject: [PATCH] Add:User listening sessions page, Update:Listening sessions to save media times and device info --- client/components/ui/Btn.vue | 8 +- .../config/users/{_id.vue => _id/index.vue} | 9 +- client/pages/config/users/_id/sessions.vue | 146 +++++++++++++++ client/plugins/constants.js | 3 +- client/plugins/init.client.js | 1 + server/controllers/LibraryItemController.js | 7 +- server/libs/isJs.js | 5 + server/libs/requestIp.js | 174 ++++++++++++++++++ server/libs/uaParserJs.js | 4 + server/managers/PlaybackSessionManager.js | 27 ++- server/objects/DeviceInfo.js | 74 ++++++++ server/objects/PlaybackSession.js | 28 ++- 12 files changed, 466 insertions(+), 20 deletions(-) rename client/pages/config/users/{_id.vue => _id/index.vue} (92%) create mode 100644 client/pages/config/users/_id/sessions.vue create mode 100644 server/libs/isJs.js create mode 100644 server/libs/requestIp.js create mode 100644 server/libs/uaParserJs.js create mode 100644 server/objects/DeviceInfo.js diff --git a/client/components/ui/Btn.vue b/client/components/ui/Btn.vue index b1d8a158..5422c278 100644 --- a/client/components/ui/Btn.vue +++ b/client/components/ui/Btn.vue @@ -32,6 +32,7 @@ export default { default: '' }, paddingX: Number, + paddingY: Number, small: Boolean, loading: Boolean, disabled: Boolean @@ -48,14 +49,17 @@ export default { if (this.small) { list.push('text-sm') if (this.paddingX === undefined) list.push('px-4') - list.push('py-1') + if (this.paddingY === undefined) list.push('py-1') } else { if (this.paddingX === undefined) list.push('px-8') - list.push('py-2') + if (this.paddingY === undefined) list.push('py-2') } if (this.paddingX !== undefined) { list.push(`px-${this.paddingX}`) } + if (this.paddingY !== undefined) { + list.push(`py-${this.paddingY}`) + } if (this.disabled) { list.push('cursor-not-allowed') } diff --git a/client/pages/config/users/_id.vue b/client/pages/config/users/_id/index.vue similarity index 92% rename from client/pages/config/users/_id.vue rename to client/pages/config/users/_id/index.vue index 45758b17..8799de85 100644 --- a/client/pages/config/users/_id.vue +++ b/client/pages/config/users/_id/index.vue @@ -22,7 +22,10 @@

Listening Stats

-

{{ listeningSessions.length }} Listening Sessions

+
+

{{ listeningSessions.length }} Listening Sessions

+ View All +

Total Time Listened:  {{ listeningTimePretty }} @@ -35,7 +38,7 @@

Last Listening Session

- {{ latestSession.displayTitle }} {{ $dateDistanceFromNow(latestSession.updatedAt) }} for {{ $elapsedPrettyExtended(this.latestSession.timeListening) }} + {{ latestSession.displayTitle }} {{ $dateDistanceFromNow(latestSession.updatedAt) }} for {{ $elapsedPrettyExtended(this.latestSession.timeListening) }}

@@ -73,7 +76,7 @@ -

Nothing read yet...

+

Nothing listened to yet...

diff --git a/client/pages/config/users/_id/sessions.vue b/client/pages/config/users/_id/sessions.vue new file mode 100644 index 00000000..ddd4e803 --- /dev/null +++ b/client/pages/config/users/_id/sessions.vue @@ -0,0 +1,146 @@ + + + + + \ No newline at end of file diff --git a/client/plugins/constants.js b/client/plugins/constants.js index 7712c680..79ad488f 100644 --- a/client/plugins/constants.js +++ b/client/plugins/constants.js @@ -28,7 +28,8 @@ const BookshelfView = { const PlayMethod = { DIRECTPLAY: 0, DIRECTSTREAM: 1, - TRANSCODE: 2 + TRANSCODE: 2, + LOCAL: 3 } const Constants = { diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index 0fab85aa..bf3a6734 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -57,6 +57,7 @@ Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => { } Vue.prototype.$secondsToTimestamp = (seconds) => { + if (!seconds) return '0:00' var _seconds = seconds var _minutes = Math.floor(seconds / 60) _seconds -= _minutes * 60 diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 070ce416..9bd34c75 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -189,8 +189,8 @@ class LibraryItemController { Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`) return res.sendStatus(404) } - const options = req.body || {} - this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, null, options, res) + + this.playbackSessionManager.startSessionRequest(req, res, null) } // POST: api/items/:id/play/:episodeId @@ -206,8 +206,7 @@ class LibraryItemController { return res.sendStatus(404) } - const options = req.body || {} - this.playbackSessionManager.startSessionRequest(req.user, libraryItem, episodeId, options, res) + this.playbackSessionManager.startSessionRequest(req, res, episodeId) } // PATCH: api/items/:id/tracks diff --git a/server/libs/isJs.js b/server/libs/isJs.js new file mode 100644 index 00000000..5f4c3439 --- /dev/null +++ b/server/libs/isJs.js @@ -0,0 +1,5 @@ +/*! + * is.js 0.9.0 + * Author: Aras Atasaygin + */ +(function (n, t) { if (typeof define === "function" && define.amd) { define(function () { return n.is = t() }) } else if (typeof exports === "object") { module.exports = t() } else { n.is = t() } })(this, function () { var n = {}; n.VERSION = "0.8.0"; n.not = {}; n.all = {}; n.any = {}; var t = Object.prototype.toString; var e = Array.prototype.slice; var r = Object.prototype.hasOwnProperty; function a(n) { return function () { return !n.apply(null, e.call(arguments)) } } function u(n) { return function () { var t = c(arguments); var e = t.length; for (var r = 0; r < e; r++) { if (!n.call(null, t[r])) { return false } } return true } } function o(n) { return function () { var t = c(arguments); var e = t.length; for (var r = 0; r < e; r++) { if (n.call(null, t[r])) { return true } } return false } } var i = { "<": function (n, t) { return n < t }, "<=": function (n, t) { return n <= t }, ">": function (n, t) { return n > t }, ">=": function (n, t) { return n >= t } }; function f(n, t) { var e = t + ""; var r = +(e.match(/\d+/) || NaN); var a = e.match(/^[<>]=?|/)[0]; return i[a] ? i[a](n, r) : n == r || r !== r } function c(t) { var r = e.call(t); var a = r.length; if (a === 1 && n.array(r[0])) { r = r[0] } return r } n.arguments = function (n) { return t.call(n) === "[object Arguments]" || n != null && typeof n === "object" && "callee" in n }; n.array = Array.isArray || function (n) { return t.call(n) === "[object Array]" }; n.boolean = function (n) { return n === true || n === false || t.call(n) === "[object Boolean]" }; n.char = function (t) { return n.string(t) && t.length === 1 }; n.date = function (n) { return t.call(n) === "[object Date]" }; n.domNode = function (t) { return n.object(t) && t.nodeType > 0 }; n.error = function (n) { return t.call(n) === "[object Error]" }; n["function"] = function (n) { return t.call(n) === "[object Function]" || typeof n === "function" }; n.json = function (n) { return t.call(n) === "[object Object]" }; n.nan = function (n) { return n !== n }; n["null"] = function (n) { return n === null }; n.number = function (e) { return n.not.nan(e) && t.call(e) === "[object Number]" }; n.object = function (n) { return Object(n) === n }; n.regexp = function (n) { return t.call(n) === "[object RegExp]" }; n.sameType = function (e, r) { var a = t.call(e); if (a !== t.call(r)) { return false } if (a === "[object Number]") { return !n.any.nan(e, r) || n.all.nan(e, r) } return true }; n.sameType.api = ["not"]; n.string = function (n) { return t.call(n) === "[object String]" }; n.undefined = function (n) { return n === void 0 }; n.windowObject = function (n) { return n != null && typeof n === "object" && "setInterval" in n }; n.empty = function (t) { if (n.object(t)) { var e = Object.getOwnPropertyNames(t).length; if (e === 0 || e === 1 && n.array(t) || e === 2 && n.arguments(t)) { return true } return false } return t === "" }; n.existy = function (n) { return n != null }; n.falsy = function (n) { return !n }; n.truthy = a(n.falsy); n.above = function (t, e) { return n.all.number(t, e) && t > e }; n.above.api = ["not"]; n.decimal = function (t) { return n.number(t) && t % 1 !== 0 }; n.equal = function (t, e) { if (n.all.number(t, e)) { return t === e && 1 / t === 1 / e } if (n.all.string(t, e) || n.all.regexp(t, e)) { return "" + t === "" + e } if (n.all.boolean(t, e)) { return t === e } return false }; n.equal.api = ["not"]; n.even = function (t) { return n.number(t) && t % 2 === 0 }; n.finite = isFinite || function (t) { return n.not.infinite(t) && n.not.nan(t) }; n.infinite = function (n) { return n === Infinity || n === -Infinity }; n.integer = function (t) { return n.number(t) && t % 1 === 0 }; n.negative = function (t) { return n.number(t) && t < 0 }; n.odd = function (t) { return n.number(t) && t % 2 === 1 }; n.positive = function (t) { return n.number(t) && t > 0 }; n.under = function (t, e) { return n.all.number(t, e) && t < e }; n.under.api = ["not"]; n.within = function (t, e, r) { return n.all.number(t, e, r) && t > e && t < r }; n.within.api = ["not"]; var l = { affirmative: /^(?:1|t(?:rue)?|y(?:es)?|ok(?:ay)?)$/, alphaNumeric: /^[A-Za-z0-9]+$/, caPostalCode: /^(?!.*[DFIOQU])[A-VXY][0-9][A-Z]\s?[0-9][A-Z][0-9]$/, creditCard: /^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/, dateString: /^(1[0-2]|0?[1-9])([\/-])(3[01]|[12][0-9]|0?[1-9])(?:\2)(?:[0-9]{2})?[0-9]{2}$/, email: /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i, eppPhone: /^\+[0-9]{1,3}\.[0-9]{4,14}(?:x.+)?$/, hexadecimal: /^(?:0x)?[0-9a-fA-F]+$/, hexColor: /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, ipv4: /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/, ipv6: /^((?=.*::)(?!.*::.+::)(::)?([\dA-F]{1,4}:(:|\b)|){5}|([\dA-F]{1,4}:){6})((([\dA-F]{1,4}((?!\3)::|:\b|$))|(?!\2\3)){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i, nanpPhone: /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/, socialSecurityNumber: /^(?!000|666)[0-8][0-9]{2}-?(?!00)[0-9]{2}-?(?!0000)[0-9]{4}$/, timeString: /^(2[0-3]|[01]?[0-9]):([0-5]?[0-9]):([0-5]?[0-9])$/, ukPostCode: /^[A-Z]{1,2}[0-9RCHNQ][0-9A-Z]?\s?[0-9][ABD-HJLNP-UW-Z]{2}$|^[A-Z]{2}-?[0-9]{4}$/, url: /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/i, usZipCode: /^[0-9]{5}(?:-[0-9]{4})?$/ }; function d(t, e) { n[t] = function (n) { return e[t].test(n) } } for (var s in l) { if (l.hasOwnProperty(s)) { d(s, l) } } n.ip = function (t) { return n.ipv4(t) || n.ipv6(t) }; n.capitalized = function (t) { if (n.not.string(t)) { return false } var e = t.split(" "); for (var r = 0; r < e.length; r++) { var a = e[r]; if (a.length) { var u = a.charAt(0); if (u !== u.toUpperCase()) { return false } } } return true }; n.endWith = function (t, e) { if (n.not.string(t)) { return false } e += ""; var r = t.length - e.length; return r >= 0 && t.indexOf(e, r) === r }; n.endWith.api = ["not"]; n.include = function (n, t) { return n.indexOf(t) > -1 }; n.include.api = ["not"]; n.lowerCase = function (t) { return n.string(t) && t === t.toLowerCase() }; n.palindrome = function (t) { if (n.not.string(t)) { return false } t = t.replace(/[^a-zA-Z0-9]+/g, "").toLowerCase(); var e = t.length - 1; for (var r = 0, a = Math.floor(e / 2); r <= a; r++) { if (t.charAt(r) !== t.charAt(e - r)) { return false } } return true }; n.space = function (t) { if (n.not.char(t)) { return false } var e = t.charCodeAt(0); return e > 8 && e < 14 || e === 32 }; n.startWith = function (t, e) { return n.string(t) && t.indexOf(e) === 0 }; n.startWith.api = ["not"]; n.upperCase = function (t) { return n.string(t) && t === t.toUpperCase() }; var F = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; var p = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"]; n.day = function (t, e) { return n.date(t) && e.toLowerCase() === F[t.getDay()] }; n.day.api = ["not"]; n.dayLightSavingTime = function (n) { var t = new Date(n.getFullYear(), 0, 1); var e = new Date(n.getFullYear(), 6, 1); var r = Math.max(t.getTimezoneOffset(), e.getTimezoneOffset()); return n.getTimezoneOffset() < r }; n.future = function (t) { var e = new Date; return n.date(t) && t.getTime() > e.getTime() }; n.inDateRange = function (t, e, r) { if (n.not.date(t) || n.not.date(e) || n.not.date(r)) { return false } var a = t.getTime(); return a > e.getTime() && a < r.getTime() }; n.inDateRange.api = ["not"]; n.inLastMonth = function (t) { return n.inDateRange(t, new Date((new Date).setMonth((new Date).getMonth() - 1)), new Date) }; n.inLastWeek = function (t) { return n.inDateRange(t, new Date((new Date).setDate((new Date).getDate() - 7)), new Date) }; n.inLastYear = function (t) { return n.inDateRange(t, new Date((new Date).setFullYear((new Date).getFullYear() - 1)), new Date) }; n.inNextMonth = function (t) { return n.inDateRange(t, new Date, new Date((new Date).setMonth((new Date).getMonth() + 1))) }; n.inNextWeek = function (t) { return n.inDateRange(t, new Date, new Date((new Date).setDate((new Date).getDate() + 7))) }; n.inNextYear = function (t) { return n.inDateRange(t, new Date, new Date((new Date).setFullYear((new Date).getFullYear() + 1))) }; n.leapYear = function (t) { return n.number(t) && (t % 4 === 0 && t % 100 !== 0 || t % 400 === 0) }; n.month = function (t, e) { return n.date(t) && e.toLowerCase() === p[t.getMonth()] }; n.month.api = ["not"]; n.past = function (t) { var e = new Date; return n.date(t) && t.getTime() < e.getTime() }; n.quarterOfYear = function (t, e) { return n.date(t) && n.number(e) && e === Math.floor((t.getMonth() + 3) / 3) }; n.quarterOfYear.api = ["not"]; n.today = function (t) { var e = new Date; var r = e.toDateString(); return n.date(t) && t.toDateString() === r }; n.tomorrow = function (t) { var e = new Date; var r = new Date(e.setDate(e.getDate() + 1)).toDateString(); return n.date(t) && t.toDateString() === r }; n.weekend = function (t) { return n.date(t) && (t.getDay() === 6 || t.getDay() === 0) }; n.weekday = a(n.weekend); n.year = function (t, e) { return n.date(t) && n.number(e) && e === t.getFullYear() }; n.year.api = ["not"]; n.yesterday = function (t) { var e = new Date; var r = new Date(e.setDate(e.getDate() - 1)).toDateString(); return n.date(t) && t.toDateString() === r }; var D = n.windowObject(typeof global == "object" && global) && global; var h = n.windowObject(typeof self == "object" && self) && self; var v = n.windowObject(typeof this == "object" && this) && this; var b = D || h || v || Function("return this")(); var g = h && h.document; var m = b.is; var w = h && h.navigator; var y = (w && w.appVersion || "").toLowerCase(); var x = (w && w.userAgent || "").toLowerCase(); var A = (w && w.vendor || "").toLowerCase(); n.android = function () { return /android/.test(x) }; n.android.api = ["not"]; n.androidPhone = function () { return /android/.test(x) && /mobile/.test(x) }; n.androidPhone.api = ["not"]; n.androidTablet = function () { return /android/.test(x) && !/mobile/.test(x) }; n.androidTablet.api = ["not"]; n.blackberry = function () { return /blackberry/.test(x) || /bb10/.test(x) }; n.blackberry.api = ["not"]; n.chrome = function (n) { var t = /google inc/.test(A) ? x.match(/(?:chrome|crios)\/(\d+)/) : null; return t !== null && f(t[1], n) }; n.chrome.api = ["not"]; n.desktop = function () { return n.not.mobile() && n.not.tablet() }; n.desktop.api = ["not"]; n.edge = function (n) { var t = x.match(/edge\/(\d+)/); return t !== null && f(t[1], n) }; n.edge.api = ["not"]; n.firefox = function (n) { var t = x.match(/(?:firefox|fxios)\/(\d+)/); return t !== null && f(t[1], n) }; n.firefox.api = ["not"]; n.ie = function (n) { var t = x.match(/(?:msie |trident.+?; rv:)(\d+)/); return t !== null && f(t[1], n) }; n.ie.api = ["not"]; n.ios = function () { return n.iphone() || n.ipad() || n.ipod() }; n.ios.api = ["not"]; n.ipad = function (n) { var t = x.match(/ipad.+?os (\d+)/); return t !== null && f(t[1], n) }; n.ipad.api = ["not"]; n.iphone = function (n) { var t = x.match(/iphone(?:.+?os (\d+))?/); return t !== null && f(t[1] || 1, n) }; n.iphone.api = ["not"]; n.ipod = function (n) { var t = x.match(/ipod.+?os (\d+)/); return t !== null && f(t[1], n) }; n.ipod.api = ["not"]; n.linux = function () { return /linux/.test(y) }; n.linux.api = ["not"]; n.mac = function () { return /mac/.test(y) }; n.mac.api = ["not"]; n.mobile = function () { return n.iphone() || n.ipod() || n.androidPhone() || n.blackberry() || n.windowsPhone() }; n.mobile.api = ["not"]; n.offline = a(n.online); n.offline.api = ["not"]; n.online = function () { return !w || w.onLine === true }; n.online.api = ["not"]; n.opera = function (n) { var t = x.match(/(?:^opera.+?version|opr)\/(\d+)/); return t !== null && f(t[1], n) }; n.opera.api = ["not"]; n.phantom = function (n) { var t = x.match(/phantomjs\/(\d+)/); return t !== null && f(t[1], n) }; n.phantom.api = ["not"]; n.safari = function (n) { var t = x.match(/version\/(\d+).+?safari/); return t !== null && f(t[1], n) }; n.safari.api = ["not"]; n.tablet = function () { return n.ipad() || n.androidTablet() || n.windowsTablet() }; n.tablet.api = ["not"]; n.touchDevice = function () { return !!g && ("ontouchstart" in h || "DocumentTouch" in h && g instanceof DocumentTouch) }; n.touchDevice.api = ["not"]; n.windows = function () { return /win/.test(y) }; n.windows.api = ["not"]; n.windowsPhone = function () { return n.windows() && /phone/.test(x) }; n.windowsPhone.api = ["not"]; n.windowsTablet = function () { return n.windows() && n.not.windowsPhone() && /touch/.test(x) }; n.windowsTablet.api = ["not"]; n.propertyCount = function (t, e) { if (n.not.object(t) || n.not.number(e)) { return false } var a = 0; for (var u in t) { if (r.call(t, u) && ++a > e) { return false } } return a === e }; n.propertyCount.api = ["not"]; n.propertyDefined = function (t, e) { return n.object(t) && n.string(e) && e in t }; n.propertyDefined.api = ["not"]; n.inArray = function (t, e) { if (n.not.array(e)) { return false } for (var r = 0; r < e.length; r++) { if (e[r] === t) { return true } } return false }; n.inArray.api = ["not"]; n.sorted = function (t, e) { if (n.not.array(t)) { return false } var r = i[e] || i[">="]; for (var a = 1; a < t.length; a++) { if (!r(t[a], t[a - 1])) { return false } } return true }; function j() { var t = n; for (var e in t) { if (r.call(t, e) && n["function"](t[e])) { var i = t[e].api || ["not", "all", "any"]; for (var f = 0; f < i.length; f++) { if (i[f] === "not") { n.not[e] = a(n[e]) } if (i[f] === "all") { n.all[e] = u(n[e]) } if (i[f] === "any") { n.any[e] = o(n[e]) } } } } } j(); n.setNamespace = function () { b.is = m; return this }; n.setRegexp = function (n, t) { for (var e in l) { if (r.call(l, e) && t === e) { l[e] = n } } }; return n }); \ No newline at end of file diff --git a/server/libs/requestIp.js b/server/libs/requestIp.js new file mode 100644 index 00000000..93dbc5e1 --- /dev/null +++ b/server/libs/requestIp.js @@ -0,0 +1,174 @@ +// SOURCE: https://github.com/pbojinov/request-ip + +"use strict"; + +function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +var is = require('./isJs'); +/** + * Parse x-forwarded-for headers. + * + * @param {string} value - The value to be parsed. + * @return {string|null} First known IP address, if any. + */ + + +function getClientIpFromXForwardedFor(value) { + if (!is.existy(value)) { + return null; + } + + if (is.not.string(value)) { + throw new TypeError("Expected a string, got \"".concat(_typeof(value), "\"")); + } // x-forwarded-for may return multiple IP addresses in the format: + // "client IP, proxy 1 IP, proxy 2 IP" + // Therefore, the right-most IP address is the IP address of the most recent proxy + // and the left-most IP address is the IP address of the originating client. + // source: http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html + // Azure Web App's also adds a port for some reason, so we'll only use the first part (the IP) + + + var forwardedIps = value.split(',').map(function (e) { + var ip = e.trim(); + + if (ip.includes(':')) { + var splitted = ip.split(':'); // make sure we only use this if it's ipv4 (ip:port) + + if (splitted.length === 2) { + return splitted[0]; + } + } + + return ip; + }); // Sometimes IP addresses in this header can be 'unknown' (http://stackoverflow.com/a/11285650). + // Therefore taking the left-most IP address that is not unknown + // A Squid configuration directive can also set the value to "unknown" (http://www.squid-cache.org/Doc/config/forwarded_for/) + + return forwardedIps.find(is.ip); +} +/** + * Determine client IP address. + * + * @param req + * @returns {string} ip - The IP address if known, defaulting to empty string if unknown. + */ + + +function getClientIp(req) { + // Server is probably behind a proxy. + if (req.headers) { + // Standard headers used by Amazon EC2, Heroku, and others. + if (is.ip(req.headers['x-client-ip'])) { + return req.headers['x-client-ip']; + } // Load-balancers (AWS ELB) or proxies. + + + var xForwardedFor = getClientIpFromXForwardedFor(req.headers['x-forwarded-for']); + + if (is.ip(xForwardedFor)) { + return xForwardedFor; + } // Cloudflare. + // @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- + // CF-Connecting-IP - applied to every request to the origin. + + + if (is.ip(req.headers['cf-connecting-ip'])) { + return req.headers['cf-connecting-ip']; + } // Fastly and Firebase hosting header (When forwared to cloud function) + + + if (is.ip(req.headers['fastly-client-ip'])) { + return req.headers['fastly-client-ip']; + } // Akamai and Cloudflare: True-Client-IP. + + + if (is.ip(req.headers['true-client-ip'])) { + return req.headers['true-client-ip']; + } // Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies. + + + if (is.ip(req.headers['x-real-ip'])) { + return req.headers['x-real-ip']; + } // (Rackspace LB and Riverbed's Stingray) + // http://www.rackspace.com/knowledge_center/article/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address + // https://splash.riverbed.com/docs/DOC-1926 + + + if (is.ip(req.headers['x-cluster-client-ip'])) { + return req.headers['x-cluster-client-ip']; + } + + if (is.ip(req.headers['x-forwarded'])) { + return req.headers['x-forwarded']; + } + + if (is.ip(req.headers['forwarded-for'])) { + return req.headers['forwarded-for']; + } + + if (is.ip(req.headers.forwarded)) { + return req.headers.forwarded; + } + } // Remote address checks. + + + if (is.existy(req.connection)) { + if (is.ip(req.connection.remoteAddress)) { + return req.connection.remoteAddress; + } + + if (is.existy(req.connection.socket) && is.ip(req.connection.socket.remoteAddress)) { + return req.connection.socket.remoteAddress; + } + } + + if (is.existy(req.socket) && is.ip(req.socket.remoteAddress)) { + return req.socket.remoteAddress; + } + + if (is.existy(req.info) && is.ip(req.info.remoteAddress)) { + return req.info.remoteAddress; + } // AWS Api Gateway + Lambda + + + if (is.existy(req.requestContext) && is.existy(req.requestContext.identity) && is.ip(req.requestContext.identity.sourceIp)) { + return req.requestContext.identity.sourceIp; + } + + return null; +} +/** + * Expose request IP as a middleware. + * + * @param {object} [options] - Configuration. + * @param {string} [options.attributeName] - Name of attribute to augment request object with. + * @return {*} + */ + + +function mw(options) { + // Defaults. + var configuration = is.not.existy(options) ? {} : options; // Validation. + + if (is.not.object(configuration)) { + throw new TypeError('Options must be an object!'); + } + + var attributeName = configuration.attributeName || 'clientIp'; + return function (req, res, next) { + var ip = getClientIp(req); + Object.defineProperty(req, attributeName, { + get: function get() { + return ip; + }, + configurable: true + }); + next(); + }; +} + +module.exports = { + getClientIpFromXForwardedFor: getClientIpFromXForwardedFor, + getClientIp: getClientIp, + mw: mw +}; diff --git a/server/libs/uaParserJs.js b/server/libs/uaParserJs.js new file mode 100644 index 00000000..da3a75b4 --- /dev/null +++ b/server/libs/uaParserJs.js @@ -0,0 +1,4 @@ +/* UAParser.js v0.7.31 + Copyright © 2012-2021 Faisal Salman + MIT License */ +(function (window, undefined) { "use strict"; var LIBVERSION = "0.7.31", EMPTY = "", UNKNOWN = "?", FUNC_TYPE = "function", UNDEF_TYPE = "undefined", OBJ_TYPE = "object", STR_TYPE = "string", MAJOR = "major", MODEL = "model", NAME = "name", TYPE = "type", VENDOR = "vendor", VERSION = "version", ARCHITECTURE = "architecture", CONSOLE = "console", MOBILE = "mobile", TABLET = "tablet", SMARTTV = "smarttv", WEARABLE = "wearable", EMBEDDED = "embedded", UA_MAX_LENGTH = 255; var AMAZON = "Amazon", APPLE = "Apple", ASUS = "ASUS", BLACKBERRY = "BlackBerry", BROWSER = "Browser", CHROME = "Chrome", EDGE = "Edge", FIREFOX = "Firefox", GOOGLE = "Google", HUAWEI = "Huawei", LG = "LG", MICROSOFT = "Microsoft", MOTOROLA = "Motorola", OPERA = "Opera", SAMSUNG = "Samsung", SONY = "Sony", XIAOMI = "Xiaomi", ZEBRA = "Zebra", FACEBOOK = "Facebook"; var extend = function (regexes, extensions) { var mergedRegexes = {}; for (var i in regexes) { if (extensions[i] && extensions[i].length % 2 === 0) { mergedRegexes[i] = extensions[i].concat(regexes[i]) } else { mergedRegexes[i] = regexes[i] } } return mergedRegexes }, enumerize = function (arr) { var enums = {}; for (var i = 0; i < arr.length; i++) { enums[arr[i].toUpperCase()] = arr[i] } return enums }, has = function (str1, str2) { return typeof str1 === STR_TYPE ? lowerize(str2).indexOf(lowerize(str1)) !== -1 : false }, lowerize = function (str) { return str.toLowerCase() }, majorize = function (version) { return typeof version === STR_TYPE ? version.replace(/[^\d\.]/g, EMPTY).split(".")[0] : undefined }, trim = function (str, len) { if (typeof str === STR_TYPE) { str = str.replace(/^\s\s*/, EMPTY).replace(/\s\s*$/, EMPTY); return typeof len === UNDEF_TYPE ? str : str.substring(0, UA_MAX_LENGTH) } }; var rgxMapper = function (ua, arrays) { var i = 0, j, k, p, q, matches, match; while (i < arrays.length && !matches) { var regex = arrays[i], props = arrays[i + 1]; j = k = 0; while (j < regex.length && !matches) { matches = regex[j++].exec(ua); if (!!matches) { for (p = 0; p < props.length; p++) { match = matches[++k]; q = props[p]; if (typeof q === OBJ_TYPE && q.length > 0) { if (q.length === 2) { if (typeof q[1] == FUNC_TYPE) { this[q[0]] = q[1].call(this, match) } else { this[q[0]] = q[1] } } else if (q.length === 3) { if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) { this[q[0]] = match ? q[1].call(this, match, q[2]) : undefined } else { this[q[0]] = match ? match.replace(q[1], q[2]) : undefined } } else if (q.length === 4) { this[q[0]] = match ? q[3].call(this, match.replace(q[1], q[2])) : undefined } } else { this[q] = match ? match : undefined } } } } i += 2 } }, strMapper = function (str, map) { for (var i in map) { if (typeof map[i] === OBJ_TYPE && map[i].length > 0) { for (var j = 0; j < map[i].length; j++) { if (has(map[i][j], str)) { return i === UNKNOWN ? undefined : i } } } else if (has(map[i], str)) { return i === UNKNOWN ? undefined : i } } return str }; var oldSafariMap = { "1.0": "/8", 1.2: "/1", 1.3: "/3", "2.0": "/412", "2.0.2": "/416", "2.0.3": "/417", "2.0.4": "/419", "?": "/" }, windowsVersionMap = { ME: "4.90", "NT 3.11": "NT3.51", "NT 4.0": "NT4.0", 2e3: "NT 5.0", XP: ["NT 5.1", "NT 5.2"], Vista: "NT 6.0", 7: "NT 6.1", 8: "NT 6.2", 8.1: "NT 6.3", 10: ["NT 6.4", "NT 10.0"], RT: "ARM" }; var regexes = { browser: [[/\b(?:crmo|crios)\/([\w\.]+)/i], [VERSION, [NAME, "Chrome"]], [/edg(?:e|ios|a)?\/([\w\.]+)/i], [VERSION, [NAME, "Edge"]], [/(opera mini)\/([-\w\.]+)/i, /(opera [mobiletab]{3,6})\b.+version\/([-\w\.]+)/i, /(opera)(?:.+version\/|[\/ ]+)([\w\.]+)/i], [NAME, VERSION], [/opios[\/ ]+([\w\.]+)/i], [VERSION, [NAME, OPERA + " Mini"]], [/\bopr\/([\w\.]+)/i], [VERSION, [NAME, OPERA]], [/(kindle)\/([\w\.]+)/i, /(lunascape|maxthon|netfront|jasmine|blazer)[\/ ]?([\w\.]*)/i, /(avant |iemobile|slim)(?:browser)?[\/ ]?([\w\.]*)/i, /(ba?idubrowser)[\/ ]?([\w\.]+)/i, /(?:ms|\()(ie) ([\w\.]+)/i, /(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|quark|qupzilla|falkon|rekonq|puffin|brave|whale|qqbrowserlite|qq)\/([-\w\.]+)/i, /(weibo)__([\d\.]+)/i], [NAME, VERSION], [/(?:\buc? ?browser|(?:juc.+)ucweb)[\/ ]?([\w\.]+)/i], [VERSION, [NAME, "UC" + BROWSER]], [/\bqbcore\/([\w\.]+)/i], [VERSION, [NAME, "WeChat(Win) Desktop"]], [/micromessenger\/([\w\.]+)/i], [VERSION, [NAME, "WeChat"]], [/konqueror\/([\w\.]+)/i], [VERSION, [NAME, "Konqueror"]], [/trident.+rv[: ]([\w\.]{1,9})\b.+like gecko/i], [VERSION, [NAME, "IE"]], [/yabrowser\/([\w\.]+)/i], [VERSION, [NAME, "Yandex"]], [/(avast|avg)\/([\w\.]+)/i], [[NAME, /(.+)/, "$1 Secure " + BROWSER], VERSION], [/\bfocus\/([\w\.]+)/i], [VERSION, [NAME, FIREFOX + " Focus"]], [/\bopt\/([\w\.]+)/i], [VERSION, [NAME, OPERA + " Touch"]], [/coc_coc\w+\/([\w\.]+)/i], [VERSION, [NAME, "Coc Coc"]], [/dolfin\/([\w\.]+)/i], [VERSION, [NAME, "Dolphin"]], [/coast\/([\w\.]+)/i], [VERSION, [NAME, OPERA + " Coast"]], [/miuibrowser\/([\w\.]+)/i], [VERSION, [NAME, "MIUI " + BROWSER]], [/fxios\/([-\w\.]+)/i], [VERSION, [NAME, FIREFOX]], [/\bqihu|(qi?ho?o?|360)browser/i], [[NAME, "360 " + BROWSER]], [/(oculus|samsung|sailfish)browser\/([\w\.]+)/i], [[NAME, /(.+)/, "$1 " + BROWSER], VERSION], [/(comodo_dragon)\/([\w\.]+)/i], [[NAME, /_/g, " "], VERSION], [/(electron)\/([\w\.]+) safari/i, /(tesla)(?: qtcarbrowser|\/(20\d\d\.[-\w\.]+))/i, /m?(qqbrowser|baiduboxapp|2345Explorer)[\/ ]?([\w\.]+)/i], [NAME, VERSION], [/(metasr)[\/ ]?([\w\.]+)/i, /(lbbrowser)/i], [NAME], [/((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w\.]+);)/i], [[NAME, FACEBOOK], VERSION], [/safari (line)\/([\w\.]+)/i, /\b(line)\/([\w\.]+)\/iab/i, /(chromium|instagram)[\/ ]([-\w\.]+)/i], [NAME, VERSION], [/\bgsa\/([\w\.]+) .*safari\//i], [VERSION, [NAME, "GSA"]], [/headlesschrome(?:\/([\w\.]+)| )/i], [VERSION, [NAME, CHROME + " Headless"]], [/ wv\).+(chrome)\/([\w\.]+)/i], [[NAME, CHROME + " WebView"], VERSION], [/droid.+ version\/([\w\.]+)\b.+(?:mobile safari|safari)/i], [VERSION, [NAME, "Android " + BROWSER]], [/(chrome|omniweb|arora|[tizenoka]{5} ?browser)\/v?([\w\.]+)/i], [NAME, VERSION], [/version\/([\w\.]+) .*mobile\/\w+ (safari)/i], [VERSION, [NAME, "Mobile Safari"]], [/version\/([\w\.]+) .*(mobile ?safari|safari)/i], [VERSION, NAME], [/webkit.+?(mobile ?safari|safari)(\/[\w\.]+)/i], [NAME, [VERSION, strMapper, oldSafariMap]], [/(webkit|khtml)\/([\w\.]+)/i], [NAME, VERSION], [/(navigator|netscape\d?)\/([-\w\.]+)/i], [[NAME, "Netscape"], VERSION], [/mobile vr; rv:([\w\.]+)\).+firefox/i], [VERSION, [NAME, FIREFOX + " Reality"]], [/ekiohf.+(flow)\/([\w\.]+)/i, /(swiftfox)/i, /(icedragon|iceweasel|camino|chimera|fennec|maemo browser|minimo|conkeror|klar)[\/ ]?([\w\.\+]+)/i, /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|palemoon|basilisk|waterfox)\/([-\w\.]+)$/i, /(firefox)\/([\w\.]+)/i, /(mozilla)\/([\w\.]+) .+rv\:.+gecko\/\d+/i, /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir|obigo|mosaic|(?:go|ice|up)[\. ]?browser)[-\/ ]?v?([\w\.]+)/i, /(links) \(([\w\.]+)/i], [NAME, VERSION]], cpu: [[/(?:(amd|x(?:(?:86|64)[-_])?|wow|win)64)[;\)]/i], [[ARCHITECTURE, "amd64"]], [/(ia32(?=;))/i], [[ARCHITECTURE, lowerize]], [/((?:i[346]|x)86)[;\)]/i], [[ARCHITECTURE, "ia32"]], [/\b(aarch64|arm(v?8e?l?|_?64))\b/i], [[ARCHITECTURE, "arm64"]], [/\b(arm(?:v[67])?ht?n?[fl]p?)\b/i], [[ARCHITECTURE, "armhf"]], [/windows (ce|mobile); ppc;/i], [[ARCHITECTURE, "arm"]], [/((?:ppc|powerpc)(?:64)?)(?: mac|;|\))/i], [[ARCHITECTURE, /ower/, EMPTY, lowerize]], [/(sun4\w)[;\)]/i], [[ARCHITECTURE, "sparc"]], [/((?:avr32|ia64(?=;))|68k(?=\))|\barm(?=v(?:[1-7]|[5-7]1)l?|;|eabi)|(?=atmel )avr|(?:irix|mips|sparc)(?:64)?\b|pa-risc)/i], [[ARCHITECTURE, lowerize]]], device: [[/\b(sch-i[89]0\d|shw-m380s|sm-[pt]\w{2,4}|gt-[pn]\d{2,4}|sgh-t8[56]9|nexus 10)/i], [MODEL, [VENDOR, SAMSUNG], [TYPE, TABLET]], [/\b((?:s[cgp]h|gt|sm)-\w+|galaxy nexus)/i, /samsung[- ]([-\w]+)/i, /sec-(sgh\w+)/i], [MODEL, [VENDOR, SAMSUNG], [TYPE, MOBILE]], [/\((ip(?:hone|od)[\w ]*);/i], [MODEL, [VENDOR, APPLE], [TYPE, MOBILE]], [/\((ipad);[-\w\),; ]+apple/i, /applecoremedia\/[\w\.]+ \((ipad)/i, /\b(ipad)\d\d?,\d\d?[;\]].+ios/i], [MODEL, [VENDOR, APPLE], [TYPE, TABLET]], [/\b((?:ag[rs][23]?|bah2?|sht?|btv)-a?[lw]\d{2})\b(?!.+d\/s)/i], [MODEL, [VENDOR, HUAWEI], [TYPE, TABLET]], [/(?:huawei|honor)([-\w ]+)[;\)]/i, /\b(nexus 6p|\w{2,4}-[atu]?[ln][01259x][012359][an]?)\b(?!.+d\/s)/i], [MODEL, [VENDOR, HUAWEI], [TYPE, MOBILE]], [/\b(poco[\w ]+)(?: bui|\))/i, /\b; (\w+) build\/hm\1/i, /\b(hm[-_ ]?note?[_ ]?(?:\d\w)?) bui/i, /\b(redmi[\-_ ]?(?:note|k)?[\w_ ]+)(?: bui|\))/i, /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite)?)(?: bui|\))/i], [[MODEL, /_/g, " "], [VENDOR, XIAOMI], [TYPE, MOBILE]], [/\b(mi[-_ ]?(?:pad)(?:[\w_ ]+))(?: bui|\))/i], [[MODEL, /_/g, " "], [VENDOR, XIAOMI], [TYPE, TABLET]], [/; (\w+) bui.+ oppo/i, /\b(cph[12]\d{3}|p(?:af|c[al]|d\w|e[ar])[mt]\d0|x9007|a101op)\b/i], [MODEL, [VENDOR, "OPPO"], [TYPE, MOBILE]], [/vivo (\w+)(?: bui|\))/i, /\b(v[12]\d{3}\w?[at])(?: bui|;)/i], [MODEL, [VENDOR, "Vivo"], [TYPE, MOBILE]], [/\b(rmx[12]\d{3})(?: bui|;|\))/i], [MODEL, [VENDOR, "Realme"], [TYPE, MOBILE]], [/\b(milestone|droid(?:[2-4x]| (?:bionic|x2|pro|razr))?:?( 4g)?)\b[\w ]+build\//i, /\bmot(?:orola)?[- ](\w*)/i, /((?:moto[\w\(\) ]+|xt\d{3,4}|nexus 6)(?= bui|\)))/i], [MODEL, [VENDOR, MOTOROLA], [TYPE, MOBILE]], [/\b(mz60\d|xoom[2 ]{0,2}) build\//i], [MODEL, [VENDOR, MOTOROLA], [TYPE, TABLET]], [/((?=lg)?[vl]k\-?\d{3}) bui| 3\.[-\w; ]{10}lg?-([06cv9]{3,4})/i], [MODEL, [VENDOR, LG], [TYPE, TABLET]], [/(lm(?:-?f100[nv]?|-[\w\.]+)(?= bui|\))|nexus [45])/i, /\blg[-e;\/ ]+((?!browser|netcast|android tv)\w+)/i, /\blg-?([\d\w]+) bui/i], [MODEL, [VENDOR, LG], [TYPE, MOBILE]], [/(ideatab[-\w ]+)/i, /lenovo ?(s[56]000[-\w]+|tab(?:[\w ]+)|yt[-\d\w]{6}|tb[-\d\w]{6})/i], [MODEL, [VENDOR, "Lenovo"], [TYPE, TABLET]], [/(?:maemo|nokia).*(n900|lumia \d+)/i, /nokia[-_ ]?([-\w\.]*)/i], [[MODEL, /_/g, " "], [VENDOR, "Nokia"], [TYPE, MOBILE]], [/(pixel c)\b/i], [MODEL, [VENDOR, GOOGLE], [TYPE, TABLET]], [/droid.+; (pixel[\daxl ]{0,6})(?: bui|\))/i], [MODEL, [VENDOR, GOOGLE], [TYPE, MOBILE]], [/droid.+ ([c-g]\d{4}|so[-gl]\w+|xq-a\w[4-7][12])(?= bui|\).+chrome\/(?![1-6]{0,1}\d\.))/i], [MODEL, [VENDOR, SONY], [TYPE, MOBILE]], [/sony tablet [ps]/i, /\b(?:sony)?sgp\w+(?: bui|\))/i], [[MODEL, "Xperia Tablet"], [VENDOR, SONY], [TYPE, TABLET]], [/ (kb2005|in20[12]5|be20[12][59])\b/i, /(?:one)?(?:plus)? (a\d0\d\d)(?: b|\))/i], [MODEL, [VENDOR, "OnePlus"], [TYPE, MOBILE]], [/(alexa)webm/i, /(kf[a-z]{2}wi)( bui|\))/i, /(kf[a-z]+)( bui|\)).+silk\//i], [MODEL, [VENDOR, AMAZON], [TYPE, TABLET]], [/((?:sd|kf)[0349hijorstuw]+)( bui|\)).+silk\//i], [[MODEL, /(.+)/g, "Fire Phone $1"], [VENDOR, AMAZON], [TYPE, MOBILE]], [/(playbook);[-\w\),; ]+(rim)/i], [MODEL, VENDOR, [TYPE, TABLET]], [/\b((?:bb[a-f]|st[hv])100-\d)/i, /\(bb10; (\w+)/i], [MODEL, [VENDOR, BLACKBERRY], [TYPE, MOBILE]], [/(?:\b|asus_)(transfo[prime ]{4,10} \w+|eeepc|slider \w+|nexus 7|padfone|p00[cj])/i], [MODEL, [VENDOR, ASUS], [TYPE, TABLET]], [/ (z[bes]6[027][012][km][ls]|zenfone \d\w?)\b/i], [MODEL, [VENDOR, ASUS], [TYPE, MOBILE]], [/(nexus 9)/i], [MODEL, [VENDOR, "HTC"], [TYPE, TABLET]], [/(htc)[-;_ ]{1,2}([\w ]+(?=\)| bui)|\w+)/i, /(zte)[- ]([\w ]+?)(?: bui|\/|\))/i, /(alcatel|geeksphone|nexian|panasonic|sony)[-_ ]?([-\w]*)/i], [VENDOR, [MODEL, /_/g, " "], [TYPE, MOBILE]], [/droid.+; ([ab][1-7]-?[0178a]\d\d?)/i], [MODEL, [VENDOR, "Acer"], [TYPE, TABLET]], [/droid.+; (m[1-5] note) bui/i, /\bmz-([-\w]{2,})/i], [MODEL, [VENDOR, "Meizu"], [TYPE, MOBILE]], [/\b(sh-?[altvz]?\d\d[a-ekm]?)/i], [MODEL, [VENDOR, "Sharp"], [TYPE, MOBILE]], [/(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|meizu|motorola|polytron)[-_ ]?([-\w]*)/i, /(hp) ([\w ]+\w)/i, /(asus)-?(\w+)/i, /(microsoft); (lumia[\w ]+)/i, /(lenovo)[-_ ]?([-\w]+)/i, /(jolla)/i, /(oppo) ?([\w ]+) bui/i], [VENDOR, MODEL, [TYPE, MOBILE]], [/(archos) (gamepad2?)/i, /(hp).+(touchpad(?!.+tablet)|tablet)/i, /(kindle)\/([\w\.]+)/i, /(nook)[\w ]+build\/(\w+)/i, /(dell) (strea[kpr\d ]*[\dko])/i, /(le[- ]+pan)[- ]+(\w{1,9}) bui/i, /(trinity)[- ]*(t\d{3}) bui/i, /(gigaset)[- ]+(q\w{1,9}) bui/i, /(vodafone) ([\w ]+)(?:\)| bui)/i], [VENDOR, MODEL, [TYPE, TABLET]], [/(surface duo)/i], [MODEL, [VENDOR, MICROSOFT], [TYPE, TABLET]], [/droid [\d\.]+; (fp\du?)(?: b|\))/i], [MODEL, [VENDOR, "Fairphone"], [TYPE, MOBILE]], [/(u304aa)/i], [MODEL, [VENDOR, "AT&T"], [TYPE, MOBILE]], [/\bsie-(\w*)/i], [MODEL, [VENDOR, "Siemens"], [TYPE, MOBILE]], [/\b(rct\w+) b/i], [MODEL, [VENDOR, "RCA"], [TYPE, TABLET]], [/\b(venue[\d ]{2,7}) b/i], [MODEL, [VENDOR, "Dell"], [TYPE, TABLET]], [/\b(q(?:mv|ta)\w+) b/i], [MODEL, [VENDOR, "Verizon"], [TYPE, TABLET]], [/\b(?:barnes[& ]+noble |bn[rt])([\w\+ ]*) b/i], [MODEL, [VENDOR, "Barnes & Noble"], [TYPE, TABLET]], [/\b(tm\d{3}\w+) b/i], [MODEL, [VENDOR, "NuVision"], [TYPE, TABLET]], [/\b(k88) b/i], [MODEL, [VENDOR, "ZTE"], [TYPE, TABLET]], [/\b(nx\d{3}j) b/i], [MODEL, [VENDOR, "ZTE"], [TYPE, MOBILE]], [/\b(gen\d{3}) b.+49h/i], [MODEL, [VENDOR, "Swiss"], [TYPE, MOBILE]], [/\b(zur\d{3}) b/i], [MODEL, [VENDOR, "Swiss"], [TYPE, TABLET]], [/\b((zeki)?tb.*\b) b/i], [MODEL, [VENDOR, "Zeki"], [TYPE, TABLET]], [/\b([yr]\d{2}) b/i, /\b(dragon[- ]+touch |dt)(\w{5}) b/i], [[VENDOR, "Dragon Touch"], MODEL, [TYPE, TABLET]], [/\b(ns-?\w{0,9}) b/i], [MODEL, [VENDOR, "Insignia"], [TYPE, TABLET]], [/\b((nxa|next)-?\w{0,9}) b/i], [MODEL, [VENDOR, "NextBook"], [TYPE, TABLET]], [/\b(xtreme\_)?(v(1[045]|2[015]|[3469]0|7[05])) b/i], [[VENDOR, "Voice"], MODEL, [TYPE, MOBILE]], [/\b(lvtel\-)?(v1[12]) b/i], [[VENDOR, "LvTel"], MODEL, [TYPE, MOBILE]], [/\b(ph-1) /i], [MODEL, [VENDOR, "Essential"], [TYPE, MOBILE]], [/\b(v(100md|700na|7011|917g).*\b) b/i], [MODEL, [VENDOR, "Envizen"], [TYPE, TABLET]], [/\b(trio[-\w\. ]+) b/i], [MODEL, [VENDOR, "MachSpeed"], [TYPE, TABLET]], [/\btu_(1491) b/i], [MODEL, [VENDOR, "Rotor"], [TYPE, TABLET]], [/(shield[\w ]+) b/i], [MODEL, [VENDOR, "Nvidia"], [TYPE, TABLET]], [/(sprint) (\w+)/i], [VENDOR, MODEL, [TYPE, MOBILE]], [/(kin\.[onetw]{3})/i], [[MODEL, /\./g, " "], [VENDOR, MICROSOFT], [TYPE, MOBILE]], [/droid.+; (cc6666?|et5[16]|mc[239][23]x?|vc8[03]x?)\)/i], [MODEL, [VENDOR, ZEBRA], [TYPE, TABLET]], [/droid.+; (ec30|ps20|tc[2-8]\d[kx])\)/i], [MODEL, [VENDOR, ZEBRA], [TYPE, MOBILE]], [/(ouya)/i, /(nintendo) ([wids3utch]+)/i], [VENDOR, MODEL, [TYPE, CONSOLE]], [/droid.+; (shield) bui/i], [MODEL, [VENDOR, "Nvidia"], [TYPE, CONSOLE]], [/(playstation [345portablevi]+)/i], [MODEL, [VENDOR, SONY], [TYPE, CONSOLE]], [/\b(xbox(?: one)?(?!; xbox))[\); ]/i], [MODEL, [VENDOR, MICROSOFT], [TYPE, CONSOLE]], [/smart-tv.+(samsung)/i], [VENDOR, [TYPE, SMARTTV]], [/hbbtv.+maple;(\d+)/i], [[MODEL, /^/, "SmartTV"], [VENDOR, SAMSUNG], [TYPE, SMARTTV]], [/(nux; netcast.+smarttv|lg (netcast\.tv-201\d|android tv))/i], [[VENDOR, LG], [TYPE, SMARTTV]], [/(apple) ?tv/i], [VENDOR, [MODEL, APPLE + " TV"], [TYPE, SMARTTV]], [/crkey/i], [[MODEL, CHROME + "cast"], [VENDOR, GOOGLE], [TYPE, SMARTTV]], [/droid.+aft(\w)( bui|\))/i], [MODEL, [VENDOR, AMAZON], [TYPE, SMARTTV]], [/\(dtv[\);].+(aquos)/i], [MODEL, [VENDOR, "Sharp"], [TYPE, SMARTTV]], [/\b(roku)[\dx]*[\)\/]((?:dvp-)?[\d\.]*)/i, /hbbtv\/\d+\.\d+\.\d+ +\([\w ]*; *(\w[^;]*);([^;]*)/i], [[VENDOR, trim], [MODEL, trim], [TYPE, SMARTTV]], [/\b(android tv|smart[- ]?tv|opera tv|tv; rv:)\b/i], [[TYPE, SMARTTV]], [/((pebble))app/i], [VENDOR, MODEL, [TYPE, WEARABLE]], [/droid.+; (glass) \d/i], [MODEL, [VENDOR, GOOGLE], [TYPE, WEARABLE]], [/droid.+; (wt63?0{2,3})\)/i], [MODEL, [VENDOR, ZEBRA], [TYPE, WEARABLE]], [/(quest( 2)?)/i], [MODEL, [VENDOR, FACEBOOK], [TYPE, WEARABLE]], [/(tesla)(?: qtcarbrowser|\/[-\w\.]+)/i], [VENDOR, [TYPE, EMBEDDED]], [/droid .+?; ([^;]+?)(?: bui|\) applew).+? mobile safari/i], [MODEL, [TYPE, MOBILE]], [/droid .+?; ([^;]+?)(?: bui|\) applew).+?(?! mobile) safari/i], [MODEL, [TYPE, TABLET]], [/\b((tablet|tab)[;\/]|focus\/\d(?!.+mobile))/i], [[TYPE, TABLET]], [/(phone|mobile(?:[;\/]| safari)|pda(?=.+windows ce))/i], [[TYPE, MOBILE]], [/(android[-\w\. ]{0,9});.+buil/i], [MODEL, [VENDOR, "Generic"]]], engine: [[/windows.+ edge\/([\w\.]+)/i], [VERSION, [NAME, EDGE + "HTML"]], [/webkit\/537\.36.+chrome\/(?!27)([\w\.]+)/i], [VERSION, [NAME, "Blink"]], [/(presto)\/([\w\.]+)/i, /(webkit|trident|netfront|netsurf|amaya|lynx|w3m|goanna)\/([\w\.]+)/i, /ekioh(flow)\/([\w\.]+)/i, /(khtml|tasman|links)[\/ ]\(?([\w\.]+)/i, /(icab)[\/ ]([23]\.[\d\.]+)/i], [NAME, VERSION], [/rv\:([\w\.]{1,9})\b.+(gecko)/i], [VERSION, NAME]], os: [[/microsoft (windows) (vista|xp)/i], [NAME, VERSION], [/(windows) nt 6\.2; (arm)/i, /(windows (?:phone(?: os)?|mobile))[\/ ]?([\d\.\w ]*)/i, /(windows)[\/ ]?([ntce\d\. ]+\w)(?!.+xbox)/i], [NAME, [VERSION, strMapper, windowsVersionMap]], [/(win(?=3|9|n)|win 9x )([nt\d\.]+)/i], [[NAME, "Windows"], [VERSION, strMapper, windowsVersionMap]], [/ip[honead]{2,4}\b(?:.*os ([\w]+) like mac|; opera)/i, /cfnetwork\/.+darwin/i], [[VERSION, /_/g, "."], [NAME, "iOS"]], [/(mac os x) ?([\w\. ]*)/i, /(macintosh|mac_powerpc\b)(?!.+haiku)/i], [[NAME, "Mac OS"], [VERSION, /_/g, "."]], [/droid ([\w\.]+)\b.+(android[- ]x86)/i], [VERSION, NAME], [/(android|webos|qnx|bada|rim tablet os|maemo|meego|sailfish)[-\/ ]?([\w\.]*)/i, /(blackberry)\w*\/([\w\.]*)/i, /(tizen|kaios)[\/ ]([\w\.]+)/i, /\((series40);/i], [NAME, VERSION], [/\(bb(10);/i], [VERSION, [NAME, BLACKBERRY]], [/(?:symbian ?os|symbos|s60(?=;)|series60)[-\/ ]?([\w\.]*)/i], [VERSION, [NAME, "Symbian"]], [/mozilla\/[\d\.]+ \((?:mobile|tablet|tv|mobile; [\w ]+); rv:.+ gecko\/([\w\.]+)/i], [VERSION, [NAME, FIREFOX + " OS"]], [/web0s;.+rt(tv)/i, /\b(?:hp)?wos(?:browser)?\/([\w\.]+)/i], [VERSION, [NAME, "webOS"]], [/crkey\/([\d\.]+)/i], [VERSION, [NAME, CHROME + "cast"]], [/(cros) [\w]+ ([\w\.]+\w)/i], [[NAME, "Chromium OS"], VERSION], [/(nintendo|playstation) ([wids345portablevuch]+)/i, /(xbox); +xbox ([^\);]+)/i, /\b(joli|palm)\b ?(?:os)?\/?([\w\.]*)/i, /(mint)[\/\(\) ]?(\w*)/i, /(mageia|vectorlinux)[; ]/i, /([kxln]?ubuntu|debian|suse|opensuse|gentoo|arch(?= linux)|slackware|fedora|mandriva|centos|pclinuxos|red ?hat|zenwalk|linpus|raspbian|plan 9|minix|risc os|contiki|deepin|manjaro|elementary os|sabayon|linspire)(?: gnu\/linux)?(?: enterprise)?(?:[- ]linux)?(?:-gnu)?[-\/ ]?(?!chrom|package)([-\w\.]*)/i, /(hurd|linux) ?([\w\.]*)/i, /(gnu) ?([\w\.]*)/i, /\b([-frentopcghs]{0,5}bsd|dragonfly)[\/ ]?(?!amd|[ix346]{1,2}86)([\w\.]*)/i, /(haiku) (\w+)/i], [NAME, VERSION], [/(sunos) ?([\w\.\d]*)/i], [[NAME, "Solaris"], VERSION], [/((?:open)?solaris)[-\/ ]?([\w\.]*)/i, /(aix) ((\d)(?=\.|\)| )[\w\.])*/i, /\b(beos|os\/2|amigaos|morphos|openvms|fuchsia|hp-ux)/i, /(unix) ?([\w\.]*)/i], [NAME, VERSION]] }; var UAParser = function (ua, extensions) { if (typeof ua === OBJ_TYPE) { extensions = ua; ua = undefined } if (!(this instanceof UAParser)) { return new UAParser(ua, extensions).getResult() } var _ua = ua || (typeof window !== UNDEF_TYPE && window.navigator && window.navigator.userAgent ? window.navigator.userAgent : EMPTY); var _rgxmap = extensions ? extend(regexes, extensions) : regexes; this.getBrowser = function () { var _browser = {}; _browser[NAME] = undefined; _browser[VERSION] = undefined; rgxMapper.call(_browser, _ua, _rgxmap.browser); _browser.major = majorize(_browser.version); return _browser }; this.getCPU = function () { var _cpu = {}; _cpu[ARCHITECTURE] = undefined; rgxMapper.call(_cpu, _ua, _rgxmap.cpu); return _cpu }; this.getDevice = function () { var _device = {}; _device[VENDOR] = undefined; _device[MODEL] = undefined; _device[TYPE] = undefined; rgxMapper.call(_device, _ua, _rgxmap.device); return _device }; this.getEngine = function () { var _engine = {}; _engine[NAME] = undefined; _engine[VERSION] = undefined; rgxMapper.call(_engine, _ua, _rgxmap.engine); return _engine }; this.getOS = function () { var _os = {}; _os[NAME] = undefined; _os[VERSION] = undefined; rgxMapper.call(_os, _ua, _rgxmap.os); return _os }; this.getResult = function () { return { ua: this.getUA(), browser: this.getBrowser(), engine: this.getEngine(), os: this.getOS(), device: this.getDevice(), cpu: this.getCPU() } }; this.getUA = function () { return _ua }; this.setUA = function (ua) { _ua = typeof ua === STR_TYPE && ua.length > UA_MAX_LENGTH ? trim(ua, UA_MAX_LENGTH) : ua; return this }; this.setUA(_ua); return this }; UAParser.VERSION = LIBVERSION; UAParser.BROWSER = enumerize([NAME, VERSION, MAJOR]); UAParser.CPU = enumerize([ARCHITECTURE]); UAParser.DEVICE = enumerize([MODEL, VENDOR, TYPE, CONSOLE, MOBILE, SMARTTV, TABLET, WEARABLE, EMBEDDED]); UAParser.ENGINE = UAParser.OS = enumerize([NAME, VERSION]); if (typeof exports !== UNDEF_TYPE) { if (typeof module !== UNDEF_TYPE && module.exports) { exports = module.exports = UAParser } exports.UAParser = UAParser } else { if (typeof define === FUNC_TYPE && define.amd) { define(function () { return UAParser }) } else if (typeof window !== UNDEF_TYPE) { window.UAParser = UAParser } } var $ = typeof window !== UNDEF_TYPE && (window.jQuery || window.Zepto); if ($ && !$.ua) { var parser = new UAParser; $.ua = parser.getResult(); $.ua.get = function () { return parser.getUA() }; $.ua.set = function (ua) { parser.setUA(ua); var result = parser.getResult(); for (var prop in result) { $.ua[prop] = result[prop] } } } })(typeof window === "object" ? window : this); \ No newline at end of file diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index 50d1ecd0..5435c29a 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -1,11 +1,16 @@ const Path = require('path') const date = require('date-and-time') +const serverVersion = require('../../package.json').version const { PlayMethod } = require('../utils/constants') const PlaybackSession = require('../objects/PlaybackSession') +const DeviceInfo = require('../objects/DeviceInfo') const Stream = require('../objects/Stream') const Logger = require('../Logger') const fs = require('fs-extra') +const uaParserJs = require('../libs/uaParserJs') +const requestIp = require('../libs/requestIp') + class PlaybackSessionManager { constructor(db, emitter, clientEmitter) { this.db = db @@ -27,8 +32,21 @@ class PlaybackSessionManager { return session ? session.stream : null } - async startSessionRequest(user, libraryItem, episodeId, options, res) { - const session = await this.startSession(user, libraryItem, episodeId, options) + getDeviceInfo(req) { + const ua = uaParserJs(req.headers['user-agent']) + const ip = requestIp.getClientIp(req) + const clientDeviceInfo = req.body ? req.body.deviceInfo || null : null // From mobile client + + const deviceInfo = new DeviceInfo() + deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion) + return deviceInfo + } + + async startSessionRequest(req, res, episodeId) { + const deviceInfo = this.getDeviceInfo(req) + + const { user, libraryItem, body: options } = req + const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options) res.json(session.toJSONForClient(libraryItem)) } @@ -84,7 +102,7 @@ class PlaybackSessionManager { res.sendStatus(200) } - async startSession(user, libraryItem, episodeId, options) { + async startSession(user, deviceInfo, libraryItem, episodeId, options) { // Close any sessions already open for user var userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id) for (const session of userSessions) { @@ -99,7 +117,7 @@ class PlaybackSessionManager { var userStartTime = 0 if (userProgress) userStartTime = Number.parseFloat(userProgress.currentTime) || 0 const newPlaybackSession = new PlaybackSession() - newPlaybackSession.setData(libraryItem, user, mediaPlayer, episodeId) + newPlaybackSession.setData(libraryItem, user, mediaPlayer, deviceInfo, userStartTime, episodeId) var audioTracks = [] if (shouldDirectPlay) { @@ -122,7 +140,6 @@ class PlaybackSessionManager { }) } - newPlaybackSession.currentTime = userStartTime newPlaybackSession.audioTracks = audioTracks // Will save on the first sync diff --git a/server/objects/DeviceInfo.js b/server/objects/DeviceInfo.js new file mode 100644 index 00000000..2f97d2ad --- /dev/null +++ b/server/objects/DeviceInfo.js @@ -0,0 +1,74 @@ +class DeviceInfo { + constructor(deviceInfo = null) { + this.ipAddress = null + + // From User Agent (see: https://www.npmjs.com/package/ua-parser-js) + this.browserName = null + this.browserVersion = null + this.osName = null + this.osVersion = null + this.deviceType = null + + // From client + this.clientVersion = null + this.manufacturer = null + this.model = null + this.sdkVersion = null // Android Only + + this.serverVersion = null + + if (deviceInfo) { + this.construct(deviceInfo) + } + } + + construct(deviceInfo) { + for (const key in deviceInfo) { + if (deviceInfo[key] !== undefined && this[key] !== undefined) { + this[key] = deviceInfo[key] + } + } + } + + toJSON() { + const obj = { + ipAddress: this.ipAddress, + browserName: this.browserName, + browserVersion: this.browserVersion, + osName: this.osName, + osVersion: this.osVersion, + deviceType: this.deviceType, + clientVersion: this.clientVersion, + manufacturer: this.manufacturer, + model: this.model, + sdkVersion: this.sdkVersion, + serverVersion: this.serverVersion + } + for (const key in obj) { + if (obj[key] === null || obj[key] === undefined) { + delete obj[key] + } + } + return obj + } + + setData(ip, ua, clientDeviceInfo, serverVersion) { + this.ipAddress = ip || null + + const uaObj = ua || {} + this.browserName = uaObj.browser.name || null + this.browserVersion = uaObj.browser.version || null + this.osName = uaObj.os.name || null + this.osVersion = uaObj.os.version || null + this.deviceType = uaObj.device.type || null + + var cdi = clientDeviceInfo || {} + this.clientVersion = cdi.clientVersion || null + this.manufacturer = cdi.manufacturer || null + this.model = cdi.model || null + this.sdkVersion = cdi.sdkVersion || null + + this.serverVersion = serverVersion || null + } +} +module.exports = DeviceInfo \ No newline at end of file diff --git a/server/objects/PlaybackSession.js b/server/objects/PlaybackSession.js index 34311928..9dd7f83f 100644 --- a/server/objects/PlaybackSession.js +++ b/server/objects/PlaybackSession.js @@ -3,6 +3,7 @@ const { getId } = require('../utils/index') const { PlayMethod } = require('../utils/constants') const BookMetadata = require('./metadata/BookMetadata') const PodcastMetadata = require('./metadata/PodcastMetadata') +const DeviceInfo = require('./DeviceInfo') class PlaybackSession { constructor(session) { @@ -21,18 +22,21 @@ class PlaybackSession { this.playMethod = null this.mediaPlayer = null + this.deviceInfo = null this.date = null this.dayOfWeek = null this.timeListening = null + this.startTime = null // media current time at start of playback + this.currentTime = 0 // Last current time set + this.startedAt = null this.updatedAt = null // Not saved in DB this.lastSave = 0 this.audioTracks = [] - this.currentTime = 0 this.stream = null if (session) { @@ -56,10 +60,13 @@ class PlaybackSession { duration: this.duration, playMethod: this.playMethod, mediaPlayer: this.mediaPlayer, + deviceInfo: this.deviceInfo ? this.deviceInfo.toJSON() : null, date: this.date, dayOfWeek: this.dayOfWeek, timeListening: this.timeListening, - lastUpdate: this.lastUpdate, + startTime: this.startTime, + currentTime: this.currentTime, + startedAt: this.startedAt, updatedAt: this.updatedAt } } @@ -80,13 +87,15 @@ class PlaybackSession { duration: this.duration, playMethod: this.playMethod, mediaPlayer: this.mediaPlayer, + deviceInfo: this.deviceInfo ? this.deviceInfo.toJSON() : null, date: this.date, dayOfWeek: this.dayOfWeek, timeListening: this.timeListening, - lastUpdate: this.lastUpdate, + startTime: this.startTime, + currentTime: this.currentTime, + startedAt: this.startedAt, updatedAt: this.updatedAt, audioTracks: this.audioTracks.map(at => at.toJSON()), - currentTime: this.currentTime, libraryItem: libraryItem.toJSONExpanded() } } @@ -101,6 +110,7 @@ class PlaybackSession { this.duration = session.duration this.playMethod = session.playMethod this.mediaPlayer = session.mediaPlayer || null + this.deviceInfo = new DeviceInfo(session.deviceInfo) this.chapters = session.chapters || [] this.mediaMetadata = null @@ -118,6 +128,9 @@ class PlaybackSession { this.dayOfWeek = session.dayOfWeek this.timeListening = session.timeListening || null + this.startTime = session.startTime || 0 + this.currentTime = session.currentTime || 0 + this.startedAt = session.startedAt this.updatedAt = session.updatedAt || null } @@ -127,7 +140,7 @@ class PlaybackSession { return Math.max(0, Math.min(this.currentTime / this.duration, 1)) } - setData(libraryItem, user, mediaPlayer, episodeId = null) { + setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) { this.id = getId('play') this.userId = user.id this.libraryItemId = libraryItem.id @@ -146,8 +159,13 @@ class PlaybackSession { } this.mediaPlayer = mediaPlayer + this.deviceInfo = deviceInfo || new DeviceInfo() + this.timeListening = 0 + this.startTime = startTime + this.currentTime = startTime + this.date = date.format(new Date(), 'YYYY-MM-DD') this.dayOfWeek = date.format(new Date(), 'dddd') this.startedAt = Date.now()