aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-03-24 13:36:47 +0100
committerChocobozzz <chocobozzz@cpy.re>2022-04-15 09:49:35 +0200
commitb211106695bb82f6c32e53306081b5262c3d109d (patch)
treefa187de1c33b0956665f5362e29af6b0f6d8bb57 /server/helpers
parent69d48ee30c9d47cddf0c3c047dc99a99dcb6e894 (diff)
downloadPeerTube-b211106695bb82f6c32e53306081b5262c3d109d.tar.gz
PeerTube-b211106695bb82f6c32e53306081b5262c3d109d.tar.zst
PeerTube-b211106695bb82f6c32e53306081b5262c3d109d.zip
Support video views/viewers stats in server
* Add "currentTime" and "event" body params to view endpoint * Merge watching and view endpoints * Introduce WatchAction AP activity * Add tables to store viewer information of local videos * Add endpoints to fetch video views/viewers stats of local videos * Refactor views/viewers handlers * Support "views" and "viewers" counters for both VOD and live videos
Diffstat (limited to 'server/helpers')
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts2
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts11
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts14
-rw-r--r--server/helpers/custom-validators/activitypub/watch-action.ts37
-rw-r--r--server/helpers/custom-validators/video-stats.ts16
-rw-r--r--server/helpers/custom-validators/video-view.ts12
-rw-r--r--server/helpers/geo-ip.ts78
7 files changed, 158 insertions, 12 deletions
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index b5c96f6e7..90a918523 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -8,6 +8,7 @@ import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './mis
8import { isPlaylistObjectValid } from './playlist' 8import { isPlaylistObjectValid } from './playlist'
9import { sanitizeAndCheckVideoCommentObject } from './video-comments' 9import { sanitizeAndCheckVideoCommentObject } from './video-comments'
10import { sanitizeAndCheckVideoTorrentObject } from './videos' 10import { sanitizeAndCheckVideoTorrentObject } from './videos'
11import { isWatchActionObjectValid } from './watch-action'
11 12
12function isRootActivityValid (activity: any) { 13function isRootActivityValid (activity: any) {
13 return isCollection(activity) || isActivity(activity) 14 return isCollection(activity) || isActivity(activity)
@@ -82,6 +83,7 @@ function isCreateActivityValid (activity: any) {
82 isDislikeActivityValid(activity.object) || 83 isDislikeActivityValid(activity.object) ||
83 isFlagActivityValid(activity.object) || 84 isFlagActivityValid(activity.object) ||
84 isPlaylistObjectValid(activity.object) || 85 isPlaylistObjectValid(activity.object) ||
86 isWatchActionObjectValid(activity.object) ||
85 87
86 isCacheFileObjectValid(activity.object) || 88 isCacheFileObjectValid(activity.object) ||
87 sanitizeAndCheckVideoCommentObject(activity.object) || 89 sanitizeAndCheckVideoCommentObject(activity.object) ||
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
index 4ee8e6fee..9d823299f 100644
--- a/server/helpers/custom-validators/activitypub/misc.ts
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -57,10 +57,19 @@ function setValidAttributedTo (obj: any) {
57 return true 57 return true
58} 58}
59 59
60function isActivityPubVideoDurationValid (value: string) {
61 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
62 return exists(value) &&
63 typeof value === 'string' &&
64 value.startsWith('PT') &&
65 value.endsWith('S')
66}
67
60export { 68export {
61 isUrlValid, 69 isUrlValid,
62 isActivityPubUrlValid, 70 isActivityPubUrlValid,
63 isBaseActivityValid, 71 isBaseActivityValid,
64 setValidAttributedTo, 72 setValidAttributedTo,
65 isObjectValid 73 isObjectValid,
74 isActivityPubVideoDurationValid
66} 75}
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 80a321117..2a2f008b9 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -4,7 +4,7 @@ import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@s
4import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos' 4import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos'
5import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' 5import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
6import { peertubeTruncate } from '../../core-utils' 6import { peertubeTruncate } from '../../core-utils'
7import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' 7import { isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
8import { isLiveLatencyModeValid } from '../video-lives' 8import { isLiveLatencyModeValid } from '../video-lives'
9import { 9import {
10 isVideoDurationValid, 10 isVideoDurationValid,
@@ -14,22 +14,13 @@ import {
14 isVideoTruncatedDescriptionValid, 14 isVideoTruncatedDescriptionValid,
15 isVideoViewsValid 15 isVideoViewsValid
16} from '../videos' 16} from '../videos'
17import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 17import { isActivityPubUrlValid, isActivityPubVideoDurationValid, isBaseActivityValid, setValidAttributedTo } from './misc'
18 18
19function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { 19function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
20 return isBaseActivityValid(activity, 'Update') && 20 return isBaseActivityValid(activity, 'Update') &&
21 sanitizeAndCheckVideoTorrentObject(activity.object) 21 sanitizeAndCheckVideoTorrentObject(activity.object)
22} 22}
23 23
24function isActivityPubVideoDurationValid (value: string) {
25 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
26 return exists(value) &&
27 typeof value === 'string' &&
28 value.startsWith('PT') &&
29 value.endsWith('S') &&
30 isVideoDurationValid(value.replace(/[^0-9]+/g, ''))
31}
32
33function sanitizeAndCheckVideoTorrentObject (video: any) { 24function sanitizeAndCheckVideoTorrentObject (video: any) {
34 if (!video || video.type !== 'Video') return false 25 if (!video || video.type !== 'Video') return false
35 26
@@ -71,6 +62,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
71 return isActivityPubUrlValid(video.id) && 62 return isActivityPubUrlValid(video.id) &&
72 isVideoNameValid(video.name) && 63 isVideoNameValid(video.name) &&
73 isActivityPubVideoDurationValid(video.duration) && 64 isActivityPubVideoDurationValid(video.duration) &&
65 isVideoDurationValid(video.duration.replace(/[^0-9]+/g, '')) &&
74 isUUIDValid(video.uuid) && 66 isUUIDValid(video.uuid) &&
75 (!video.category || isRemoteNumberIdentifierValid(video.category)) && 67 (!video.category || isRemoteNumberIdentifierValid(video.category)) &&
76 (!video.licence || isRemoteNumberIdentifierValid(video.licence)) && 68 (!video.licence || isRemoteNumberIdentifierValid(video.licence)) &&
diff --git a/server/helpers/custom-validators/activitypub/watch-action.ts b/server/helpers/custom-validators/activitypub/watch-action.ts
new file mode 100644
index 000000000..b9ffa63f6
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/watch-action.ts
@@ -0,0 +1,37 @@
1import { WatchActionObject } from '@shared/models'
2import { exists, isDateValid, isUUIDValid } from '../misc'
3import { isVideoTimeValid } from '../video-view'
4import { isActivityPubVideoDurationValid, isObjectValid } from './misc'
5
6function isWatchActionObjectValid (action: WatchActionObject) {
7 return exists(action) &&
8 action.type === 'WatchAction' &&
9 isObjectValid(action.id) &&
10 isActivityPubVideoDurationValid(action.duration) &&
11 isDateValid(action.startTime) &&
12 isDateValid(action.endTime) &&
13 isLocationValid(action.location) &&
14 isUUIDValid(action.uuid) &&
15 isObjectValid(action.object) &&
16 isWatchSectionsValid(action.watchSections)
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 isWatchActionObjectValid
23}
24
25// ---------------------------------------------------------------------------
26
27function isLocationValid (location: any) {
28 if (!location) return true
29
30 return typeof location === 'object' && typeof location.addressCountry === 'string'
31}
32
33function isWatchSectionsValid (sections: WatchActionObject['watchSections']) {
34 return Array.isArray(sections) && sections.every(s => {
35 return isVideoTimeValid(s.startTimestamp) && isVideoTimeValid(s.endTimestamp)
36 })
37}
diff --git a/server/helpers/custom-validators/video-stats.ts b/server/helpers/custom-validators/video-stats.ts
new file mode 100644
index 000000000..1e22f0654
--- /dev/null
+++ b/server/helpers/custom-validators/video-stats.ts
@@ -0,0 +1,16 @@
1import { VideoStatsTimeserieMetric } from '@shared/models'
2
3const validMetrics = new Set<VideoStatsTimeserieMetric>([
4 'viewers',
5 'aggregateWatchTime'
6])
7
8function isValidStatTimeserieMetric (value: VideoStatsTimeserieMetric) {
9 return validMetrics.has(value)
10}
11
12// ---------------------------------------------------------------------------
13
14export {
15 isValidStatTimeserieMetric
16}
diff --git a/server/helpers/custom-validators/video-view.ts b/server/helpers/custom-validators/video-view.ts
new file mode 100644
index 000000000..091c92083
--- /dev/null
+++ b/server/helpers/custom-validators/video-view.ts
@@ -0,0 +1,12 @@
1import { exists } from './misc'
2
3function isVideoTimeValid (value: number, videoDuration?: number) {
4 if (value < 0) return false
5 if (exists(videoDuration) && value > videoDuration) return false
6
7 return true
8}
9
10export {
11 isVideoTimeValid
12}
diff --git a/server/helpers/geo-ip.ts b/server/helpers/geo-ip.ts
new file mode 100644
index 000000000..4ba7011c2
--- /dev/null
+++ b/server/helpers/geo-ip.ts
@@ -0,0 +1,78 @@
1import { pathExists, writeFile } from 'fs-extra'
2import maxmind, { CountryResponse, Reader } from 'maxmind'
3import { join } from 'path'
4import { CONFIG } from '@server/initializers/config'
5import { logger, loggerTagsFactory } from './logger'
6import { isBinaryResponse, peertubeGot } from './requests'
7
8const lTags = loggerTagsFactory('geo-ip')
9
10const mmbdFilename = 'dbip-country-lite-latest.mmdb'
11const mmdbPath = join(CONFIG.STORAGE.BIN_DIR, mmbdFilename)
12
13export class GeoIP {
14 private static instance: GeoIP
15
16 private reader: Reader<CountryResponse>
17
18 private constructor () {
19 }
20
21 async safeCountryISOLookup (ip: string): Promise<string> {
22 if (CONFIG.GEO_IP.ENABLED === false) return null
23
24 await this.initReaderIfNeeded()
25
26 try {
27 const result = this.reader.get(ip)
28 if (!result) return null
29
30 return result.country.iso_code
31 } catch (err) {
32 logger.error('Cannot get country from IP.', { err })
33
34 return null
35 }
36 }
37
38 async updateDatabase () {
39 if (CONFIG.GEO_IP.ENABLED === false) return
40
41 const url = CONFIG.GEO_IP.COUNTRY.DATABASE_URL
42
43 logger.info('Updating GeoIP database from %s.', url, lTags())
44
45 const gotOptions = { context: { bodyKBLimit: 200_000 }, responseType: 'buffer' as 'buffer' }
46
47 try {
48 const gotResult = await peertubeGot(url, gotOptions)
49
50 if (!isBinaryResponse(gotResult)) {
51 throw new Error('Not a binary response')
52 }
53
54 await writeFile(mmdbPath, gotResult.body)
55
56 // Reini reader
57 this.reader = undefined
58
59 logger.info('GeoIP database updated %s.', mmdbPath, lTags())
60 } catch (err) {
61 logger.error('Cannot update GeoIP database from %s.', url, { err, ...lTags() })
62 }
63 }
64
65 private async initReaderIfNeeded () {
66 if (!this.reader) {
67 if (!await pathExists(mmdbPath)) {
68 await this.updateDatabase()
69 }
70
71 this.reader = await maxmind.open(mmdbPath)
72 }
73 }
74
75 static get Instance () {
76 return this.instance || (this.instance = new this())
77 }
78}