はじまりの大地

このコミットが含まれているのは:
2024-07-15 09:14:04 +09:00
コミット 6632905f32
3501個のファイルの変更1439465行の追加0行の削除
バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 7.8 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 40 KiB

+11
ファイルの表示
@@ -0,0 +1,11 @@
extends ../common/greetings
include ../common/mixins.pug
block title
| New message on abuse report
block content
p
| A new message by #{messageAccountName} was posted on #[a(href=abuseUrl) abuse report ##{abuseId}] on #{instanceName}
blockquote #{messageText}
br(style="display: none;")
+9
ファイルの表示
@@ -0,0 +1,9 @@
extends ../common/greetings
include ../common/mixins.pug
block title
| Abuse report state changed
block content
p
| #[a(href=abuseUrl) Your abuse report ##{abuseId}] on #{instanceName} has been #{isAccepted ? 'accepted' : 'rejected'}
+14
ファイルの表示
@@ -0,0 +1,14 @@
extends ../common/greetings
include ../common/mixins.pug
block title
| An account is pending moderation
block content
p
| #[a(href=WEBSERVER.URL) #{instanceName}] received an abuse report for the #{isLocal ? '' : 'remote '}account
a(href=accountUrl) #{accountDisplayName}
p The reporter, #{reporter}, cited the following reason(s):
blockquote #{reason}
br(style="display: none;")
+258
ファイルの表示
@@ -0,0 +1,258 @@
//-
The email background color is defined in three places:
1. body tag: for most email clients
2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
3. mso conditional: For Windows 10 Mail
- var backgroundColor = "#fff";
- var mainColor = "#f2690d";
doctype html
head
// This template is heavily adapted from the Cerberus Fluid template. Kudos to them!
meta(charset='utf-8')
//- utf-8 works for most cases
meta(name='viewport' content='width=device-width')
//- Forcing initial-scale shouldn't be necessary
meta(http-equiv='X-UA-Compatible' content='IE=edge')
//- Use the latest (edge) version of IE rendering engine
meta(name='x-apple-disable-message-reformatting')
//- Disable auto-scale in iOS 10 Mail entirely
meta(name='format-detection' content='telephone=no,address=no,email=no,date=no,url=no')
//- Tell iOS not to automatically link certain text strings.
meta(name='color-scheme' content='light')
meta(name='supported-color-schemes' content='light')
//- The title tag shows in email notifications, like Android 4.4.
title #{subject}
//- What it does: Makes background images in 72ppi Outlook render at correct size.
//if gte mso 9
xml
o:officedocumentsettings
o:allowpng
o:pixelsperinch 96
//- CSS Reset : BEGIN
style.
/* What it does: Tells the email client that only light styles are provided but the client can transform them to dark. A duplicate of meta color-scheme meta tag above. */
:root {
color-scheme: light;
supported-color-schemes: light;
}
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: forces Samsung Android mail clients to use the entire viewport */
#MessageViewBody, #MessageWebViewDiv{
width: 100% !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode:bicubic;
}
/* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
a {
text-decoration: none;
}
a:not(.nocolor) {
color: #{mainColor};
}
a.nocolor {
color: inherit !important;
}
/* What it does: A work-around for email clients meddling in triggered links. */
a[x-apple-data-detectors], /* iOS */
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
.im {
color: inherit !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u ~ div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u ~ div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u ~ div .email-container {
min-width: 414px !important;
}
}
//- CSS Reset : END
//- CSS for PeerTube : START
style.
blockquote {
margin-left: 0;
padding-left: 20px;
border-left: 2px solid #f2690d;
}
//- CSS for PeerTube : END
//- Progressive Enhancements : BEGIN
style.
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td-primary:hover,
.button-a-primary:hover {
background: #555555 !important;
border-color: #555555 !important;
}
/* Media Queries */
@media screen and (max-width: 600px) {
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
}
}
//- Progressive Enhancements : END
body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #{backgroundColor};")
center(role='article' aria-roledescription='email' lang='en' style='width: 100%; background-color: #{backgroundColor};')
//if mso | IE
table(role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='background-color: #fff;')
tr
td
//- Visually Hidden Preheader Text : BEGIN
div(style='max-height:0; overflow:hidden; mso-hide:all;' aria-hidden='true')
block preheader
//- Visually Hidden Preheader Text : END
//- Create white space after the desired preview text so email clients dont pull other distracting text into the inbox preview. Extend as necessary.
//- Preview Text Spacing Hack : BEGIN
div(style='display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;')
| ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
//- Preview Text Spacing Hack : END
//-
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.
2. MSO tags for Desktop Windows Outlook enforce a 600px width.
.email-container(style='max-width: 600px; margin: 0 auto;')
//if mso
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='600')
tr
td
//- Email Body : BEGIN
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
//- 1 Column Text + Button : BEGIN
tr
td(style='background-color: #ffffff;')
table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
tr
td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
tr
td(width="40px")
img(src=`${WEBSERVER.URL}/client/assets/images/icons/icon-192x192.png` width="auto" height="30px" alt="" border="0" style="height: 30px; background: #ffffff; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555;")
td
h1(style='margin: 10px 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;')
block title
if title
| #{title}
else
| Something requires your attention
p(style='margin: 0;')
block body
if action
tr
td(style='padding: 0 20px;')
//- Button : BEGIN
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' style='margin: auto;')
tr
td.button-td.button-td-primary(style='border-radius: 4px; background: #222222;')
a.button-a.button-a-primary(href=action.url style='background: #222222; border: 1px solid #000000; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;') #{action.text}
//- Button : END
//- 1 Column Text + Button : END
//- Clear Spacer : BEGIN
tr
td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;')
br
//- Clear Spacer : END
//- Email Body : END
//- Email Footer : BEGIN
unless hideNotificationPreferencesLink
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
tr
td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
webversion
a.nocolor(href=`${WEBSERVER.URL}/my-account/notifications` style='color: #cccccc; font-weight: bold;') View in your notifications
br
tr
td(style='padding: 20px; padding-top: 10px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
unsubscribe
a.nocolor(href=`${WEBSERVER.URL}/my-account/settings#notifications` style='color: #888888;') Manage your notification preferences in your profile
br
//- Email Footer : END
//if mso
//- Full Bleed Background Section : BEGIN
table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style=`background-color: ${mainColor};`)
tr
td
.email-container(align='center' style='max-width: 600px; margin: auto;')
//if mso
table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='600' align='center')
tr
td
table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
tr
td(style='padding: 20px; text-align: left; font-family: sans-serif; font-size: 12px; line-height: 20px; color: #ffffff;')
table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
tr
td(valign="top") #[a(href="https://github.com/Chocobozzz/PeerTube" style="color: white !important") PeerTube © 2015-#{new Date().getFullYear()}] #[a(href="https://github.com/Chocobozzz/PeerTube/blob/master/CREDITS.md" style="color: white !important") PeerTube Contributors]
//if mso
//- Full Bleed Background Section : END
//if mso | IE
+11
ファイルの表示
@@ -0,0 +1,11 @@
extends base
block body
if username
p Hi #{username},
else
p Hi,
block content
p
| Cheers,#[br]
| #{EMAIL.BODY.SIGNATURE}
+4
ファイルの表示
@@ -0,0 +1,4 @@
extends greetings
block content
p !{text}
+7
ファイルの表示
@@ -0,0 +1,7 @@
mixin channel(channel)
- var handle = `${channel.name}@${channel.host}`
| #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}]
mixin account(account)
- var handle = `${account.name}@${account.host}`
| #[a(href=`${WEBSERVER.URL}/accounts/${handle}` title=handle) #{account.displayName}]
+9
ファイルの表示
@@ -0,0 +1,9 @@
extends ../common/greetings
block title
| Someone just used the contact form
block content
p #{fromName} sent you a message via the contact form on #[a(href=WEBSERVER.URL) #{instanceName}]:
blockquote(style='white-space: pre-wrap') #{body}
p You can contact them at #[a(href=`mailto:${fromEmail}`) #{fromEmail}], or simply reply to this email to get in touch.
+9
ファイルの表示
@@ -0,0 +1,9 @@
extends ../common/greetings
block title
| New follower on your channel
block content
p.
Your #{followType} #[a(href=followingUrl) #{followingName}] has a new subscriber:
#[a(href=followerUrl) #{followerName}].
+10
ファイルの表示
@@ -0,0 +1,10 @@
extends ../common/greetings
block title
| Password creation for your account
block content
p.
Welcome to #[a(href=WEBSERVER.URL) #{instanceName}]. Your username is: #{username}.
Please set your password by following #[a(href=createPasswordUrl) this link]: #[a(href=createPasswordUrl) #{createPasswordUrl}]
(this link will expire within seven days).
+12
ファイルの表示
@@ -0,0 +1,12 @@
extends ../common/greetings
block title
| Password reset for your account
block content
p.
A reset password procedure for your account #{username} has been requested on #[a(href=WEBSERVER.URL) #{instanceName}].
Please follow #[a(href=resetPasswordUrl) this link] to reset it: #[a(href=resetPasswordUrl) #{resetPasswordUrl}]
(the link will expire within 1 hour).
p.
If you are not the person who initiated this request, please ignore this email.
+9
ファイルの表示
@@ -0,0 +1,9 @@
extends ../common/greetings
block title
| New PeerTube version available
block content
p
| A new version of PeerTube is available: #{latestVersion}.
| You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube].
+9
ファイルの表示
@@ -0,0 +1,9 @@
extends ../common/greetings
block title
| New plugin version available
block content
p
| A new version of the plugin/theme #{pluginName} is available: #{latestVersion}.
| You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface].
+9
ファイルの表示
@@ -0,0 +1,9 @@
extends ../common/greetings
include ../common/mixins.pug
block title
| Your export archive has been created
block content
p
| Your export archive has been created. You can download it in #[a(href=exportsUrl) your account export page].
+12
ファイルの表示
@@ -0,0 +1,12 @@
extends ../common/greetings
include ../common/mixins.pug
block title
| Failed to create your export archive
block content
p
| We are sorry but the generation of your export archive has failed:
blockquote !{errorMessage}
p
| Please contact your administrator if the problem occurs again.
+55
ファイルの表示
@@ -0,0 +1,55 @@
extends ../common/greetings
include ../common/mixins.pug
mixin displaySummary(stats)
ul
if stats.success
li Imported: #{stats.success}
if stats.duplicates
li Not imported as considered duplicate: #{stats.duplicates}
if stats.errors
li Not imported due to error: #{stats.errors}
block title
| Your archive import has finished
block content
p Your archive import has finished. Here is the summary of imported objects:
ul
li
strong User settings:
+displaySummary(resultStats.userSettings)
li
strong Account (name, description, avatar...):
+displaySummary(resultStats.account)
li
strong Blocklist:
+displaySummary(resultStats.blocklist)
li
strong Channels:
+displaySummary(resultStats.channels)
li
strong Likes:
+displaySummary(resultStats.likes)
li
strong Dislikes:
+displaySummary(resultStats.dislikes)
li
strong Subscriptions:
+displaySummary(resultStats.following)
li
strong Video Playlists:
+displaySummary(resultStats.videoPlaylists)
li
strong Videos:
+displaySummary(resultStats.videos)
li
strong Video history:
+displaySummary(resultStats.userVideoHistory)
li
strong Watched Words Lists:
+displaySummary(resultStats.watchedWordsLists)
li
strong Comment auto tag policies:
+displaySummary(resultStats.commentAutoTagPolicies)
+12
ファイルの表示
@@ -0,0 +1,12 @@
extends ../common/greetings
include ../common/mixins.pug
block title
| Failed to import your archive
block content
p
| We are sorry but the import of your archive has failed:
blockquote !{errorMessage}
p
| Please contact your administrator if the problem occurs again.
+10
ファイルの表示
@@ -0,0 +1,10 @@
extends ../common/greetings
block title
| A new user registered
block content
- var mail = user.email || user.pendingEmail;
p
| User #[a(href=`${WEBSERVER.URL}/accounts/${user.username}`) #{user.username}] just registered.
| You might want to contact them at #[a(href=`mailto:${mail}`) #{mail}].
+10
ファイルの表示
@@ -0,0 +1,10 @@
extends ../common/greetings
block title
| Congratulation #{username}, your registration request has been accepted!
block content
p Your registration request has been accepted.
p Moderators sent you the following message:
blockquote(style='white-space: pre-wrap') #{moderationResponse}
p Your account has been created and you can login on #[a(href=loginLink) #{loginLink}]
@@ -0,0 +1,9 @@
extends ../common/greetings
block title
| Registration request of your account #{username} has rejected
block content
p Your registration request has been rejected.
p Moderators sent you the following message:
blockquote(style='white-space: pre-wrap') #{moderationResponse}
+9
ファイルの表示
@@ -0,0 +1,9 @@
extends ../common/greetings
block title
| A new user wants to register
block content
p User #{registration.username} wants to register on your PeerTube instance with the following reason:
blockquote(style='white-space: pre-wrap') #{registration.registrationReason}
p You can accept or reject the registration request in the #[a(href=`${WEBSERVER.URL}/admin/moderation/registrations/list`) administration].
+19
ファイルの表示
@@ -0,0 +1,19 @@
extends ../common/greetings
block title
| Email verification
block content
if isRegistrationRequest
p You just requested an account on #[a(href=WEBSERVER.URL) #{instanceName}].
else
p You just created an account on #[a(href=WEBSERVER.URL) #{instanceName}].
if isRegistrationRequest
p To complete your registration request you must verify your email first!
else
p To start using your account you must verify your email first!
p Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you.
p If you can't see the verification link above you can use the following link #[a(href=verifyEmailUrl) #{verifyEmailUrl}]
p If you are not the person who initiated this request, please ignore this email.
+18
ファイルの表示
@@ -0,0 +1,18 @@
extends ../common/greetings
include ../common/mixins.pug
block title
| A video is pending moderation
block content
p
| #[a(href=WEBSERVER.URL) #{instanceName}] received an abuse report for the #{isLocal ? '' : 'remote '}video "
a(href=videoUrl) #{videoName}
| " by #[+channel(videoChannel)]
if videoPublishedAt
| , published the #{videoPublishedAt}.
else
| , uploaded the #{videoCreatedAt} but not yet published.
p The reporter, #{reporter}, cited the following reason(s):
blockquote #{reason}
br(style="display: none;")
+17
ファイルの表示
@@ -0,0 +1,17 @@
extends ../common/greetings
include ../common/mixins
block title
| A video is pending moderation
block content
p
| A recently added video was auto-blacklisted and requires moderator review before going public:
|
a(href=videoUrl) #{videoName}
|
| by #[+channel(channel)].
p.
Apart from the publisher and the moderation team, no one will be able to see the video until you
unblacklist it. If you trust the publisher, any admin can whitelist the user for later videos so
that they don't require approval before going public.
+16
ファイルの表示
@@ -0,0 +1,16 @@
extends ../common/greetings
include ../common/mixins.pug
block title
| A comment is pending moderation
block content
p
| #[a(href=WEBSERVER.URL) #{instanceName}] received an abuse report for the #{isLocal ? '' : 'remote '}
a(href=commentUrl) comment on video "#{videoName}"
| of #{flaggedAccount}
| created on #{commentCreatedAt}
p The reporter, #{reporter}, cited the following reason(s):
blockquote #{reason}
br(style="display: none;")
+11
ファイルの表示
@@ -0,0 +1,11 @@
extends ../common/greetings
block title
| Someone mentioned you
block content
p.
#[a(href=accountUrl title=handle) #{accountName}] mentioned you in a comment on video
"#[a(href=videoUrl) #{video.name}]":
blockquote !{commentHtml}
br(style="display: none;")
+16
ファイルの表示
@@ -0,0 +1,16 @@
extends ../common/greetings
block title
| Someone commented your video
block content
p.
#[a(href=accountUrl title=handle) #{accountName}] added a comment on your video
"#[a(href=videoUrl) #{video.name}]":
blockquote !{commentHtml}
if requiresApproval
| This comment requires approval.
br(style="display: none;")
+558
ファイルの表示
@@ -0,0 +1,558 @@
import {
HttpStatusCode,
VideoChaptersObject,
VideoCommentObject,
VideoPlaylistPrivacy,
VideoPrivacy,
VideoRateType
} from '@peertube/peertube-models'
import { activityPubCollectionPagination } from '@server/lib/activitypub/collection.js'
import { getContextFilter } from '@server/lib/activitypub/context.js'
import { buildChaptersAPHasPart } from '@server/lib/activitypub/video-chapters.js'
import { InternalEventEmitter } from '@server/lib/internal-event-emitter.js'
import { getServerActor } from '@server/models/application/application.js'
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models/index.js'
import cors from 'cors'
import express from 'express'
import { activityPubContextify } from '../../helpers/activity-pub-utils.js'
import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants.js'
import { audiencify, getAudience } from '../../lib/activitypub/audience.js'
import { buildAnnounceWithVideoAudience, buildApprovalActivity, buildLikeActivity } from '../../lib/activitypub/send/index.js'
import { buildCreateActivity } from '../../lib/activitypub/send/send-create.js'
import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike.js'
import {
getLocalVideoChaptersActivityPubUrl,
getLocalVideoCommentsActivityPubUrl,
getLocalVideoDislikesActivityPubUrl,
getLocalVideoLikesActivityPubUrl,
getLocalVideoSharesActivityPubUrl
} from '../../lib/activitypub/url.js'
import {
apVideoChaptersSetCacheKey,
buildAPVideoChaptersGroupsCache,
cacheRoute,
cacheRouteFactory
} from '../../middlewares/cache/cache.js'
import {
activityPubRateLimiter,
asyncMiddleware,
ensureIsLocalChannel,
executeIfActivityPub,
localAccountValidator,
videoChannelsNameWithHostValidator,
videosCustomGetValidator,
videosShareValidator
} from '../../middlewares/index.js'
import {
getAccountVideoRateValidatorFactory,
getVideoLocalViewerValidator,
videoCommentGetValidator
} from '../../middlewares/validators/index.js'
import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy.js'
import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists.js'
import { AccountVideoRateModel } from '../../models/account/account-video-rate.js'
import { AccountModel } from '../../models/account/account.js'
import { ActorFollowModel } from '../../models/actor/actor-follow.js'
import { VideoCommentModel } from '../../models/video/video-comment.js'
import { VideoPlaylistModel } from '../../models/video/video-playlist.js'
import { VideoShareModel } from '../../models/video/video-share.js'
import { activityPubResponse } from './utils.js'
const activityPubClientRouter = express.Router()
activityPubClientRouter.use(cors())
// Intercept ActivityPub client requests
activityPubClientRouter.get(
[ '/accounts?/:name', '/accounts?/:name/video-channels', '/a/:name', '/a/:name/video-channels' ],
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(localAccountValidator),
asyncMiddleware(accountController)
)
activityPubClientRouter.get('/accounts?/:name/followers',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(localAccountValidator),
asyncMiddleware(accountFollowersController)
)
activityPubClientRouter.get('/accounts?/:name/following',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(localAccountValidator),
asyncMiddleware(accountFollowingController)
)
activityPubClientRouter.get('/accounts?/:name/playlists',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(localAccountValidator),
asyncMiddleware(accountPlaylistsController)
)
activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
executeIfActivityPub,
activityPubRateLimiter,
cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
asyncMiddleware(getAccountVideoRateValidatorFactory('like')),
asyncMiddleware(getAccountVideoRateFactory('like'))
)
activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
executeIfActivityPub,
activityPubRateLimiter,
cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')),
asyncMiddleware(getAccountVideoRateFactory('dislike'))
)
activityPubClientRouter.get(
[ '/videos/watch/:id', '/w/:id' ],
executeIfActivityPub,
activityPubRateLimiter,
cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
asyncMiddleware(videosCustomGetValidator('all')),
asyncMiddleware(videoController)
)
activityPubClientRouter.get('/videos/watch/:id/activity',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('all')),
asyncMiddleware(videoController)
)
activityPubClientRouter.get('/videos/watch/:id/announces',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoAnnouncesController)
)
activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosShareValidator),
asyncMiddleware(videoAnnounceController)
)
activityPubClientRouter.get('/videos/watch/:id/likes',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoLikesController)
)
activityPubClientRouter.get('/videos/watch/:id/dislikes',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoDislikesController)
)
// ---------------------------------------------------------------------------
activityPubClientRouter.get('/videos/watch/:id/comments',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoCommentsController)
)
activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/approve-reply',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoCommentGetValidator),
asyncMiddleware(videoCommentApprovedController)
)
activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoCommentGetValidator),
asyncMiddleware(videoCommentController)
)
activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoCommentGetValidator),
asyncMiddleware(videoCommentController)
)
// ---------------------------------------------------------------------------
const { middleware: chaptersCacheRouteMiddleware, instance: chaptersApiCache } = cacheRouteFactory()
InternalEventEmitter.Instance.on('chapters-updated', ({ video }) => {
if (video.remote) return
chaptersApiCache.clearGroupSafe(buildAPVideoChaptersGroupsCache({ videoId: video.uuid }))
})
activityPubClientRouter.get('/videos/watch/:id/chapters',
executeIfActivityPub,
activityPubRateLimiter,
apVideoChaptersSetCacheKey,
chaptersCacheRouteMiddleware(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoChaptersController)
)
// ---------------------------------------------------------------------------
activityPubClientRouter.get(
[ '/video-channels/:nameWithHost', '/video-channels/:nameWithHost/videos', '/c/:nameWithHost', '/c/:nameWithHost/videos' ],
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(videoChannelController)
)
activityPubClientRouter.get('/video-channels/:nameWithHost/followers',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(videoChannelFollowersController)
)
activityPubClientRouter.get('/video-channels/:nameWithHost/following',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(videoChannelFollowingController)
)
activityPubClientRouter.get('/video-channels/:nameWithHost/playlists',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(videoChannelPlaylistsController)
)
activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoFileRedundancyGetValidator),
asyncMiddleware(videoRedundancyController)
)
activityPubClientRouter.get('/redundancy/streaming-playlists/:streamingPlaylistType/:videoId',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoPlaylistRedundancyGetValidator),
asyncMiddleware(videoRedundancyController)
)
activityPubClientRouter.get(
[ '/video-playlists/:playlistId', '/videos/watch/playlist/:playlistId', '/w/p/:playlistId' ],
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoPlaylistsGetValidator('all')),
asyncMiddleware(videoPlaylistController)
)
activityPubClientRouter.get('/video-playlists/:playlistId/videos/:playlistElementId',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videoPlaylistElementAPGetValidator),
asyncMiddleware(videoPlaylistElementController)
)
activityPubClientRouter.get('/videos/local-viewer/:localViewerId',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(getVideoLocalViewerValidator),
asyncMiddleware(getVideoLocalViewerController)
)
// ---------------------------------------------------------------------------
export {
activityPubClientRouter
}
// ---------------------------------------------------------------------------
async function accountController (req: express.Request, res: express.Response) {
const account = res.locals.account
return activityPubResponse(activityPubContextify(await account.toActivityPubObject(), 'Actor', getContextFilter()), res)
}
async function accountFollowersController (req: express.Request, res: express.Response) {
const account = res.locals.account
const activityPubResult = await actorFollowers(req, account.Actor)
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
}
async function accountFollowingController (req: express.Request, res: express.Response) {
const account = res.locals.account
const activityPubResult = await actorFollowing(req, account.Actor)
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
}
async function accountPlaylistsController (req: express.Request, res: express.Response) {
const account = res.locals.account
const activityPubResult = await actorPlaylists(req, { account })
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
}
async function videoChannelPlaylistsController (req: express.Request, res: express.Response) {
const channel = res.locals.videoChannel
const activityPubResult = await actorPlaylists(req, { channel })
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
}
function getAccountVideoRateFactory (rateType: VideoRateType) {
return (req: express.Request, res: express.Response) => {
const accountVideoRate = res.locals.accountVideoRate
const byActor = accountVideoRate.Account.Actor
const APObject = rateType === 'like'
? buildLikeActivity(accountVideoRate.url, byActor, accountVideoRate.Video)
: buildDislikeActivity(accountVideoRate.url, byActor, accountVideoRate.Video)
return activityPubResponse(activityPubContextify(APObject, 'Rate', getContextFilter()), res)
}
}
async function videoController (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
if (redirectIfNotOwned(video.url, res)) return
// We need captions to render AP object
const videoAP = await video.lightAPToFullAP(undefined)
const audience = getAudience(videoAP.VideoChannel.Account.Actor, videoAP.privacy === VideoPrivacy.PUBLIC)
const videoObject = audiencify(await videoAP.toActivityPubObject(), audience)
if (req.path.endsWith('/activity')) {
const data = buildCreateActivity(videoAP.url, video.VideoChannel.Account.Actor, videoObject, audience)
return activityPubResponse(activityPubContextify(data, 'Video', getContextFilter()), res)
}
return activityPubResponse(activityPubContextify(videoObject, 'Video', getContextFilter()), res)
}
async function videoAnnounceController (req: express.Request, res: express.Response) {
const share = res.locals.videoShare
if (redirectIfNotOwned(share.url, res)) return
const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.videoAll, undefined)
return activityPubResponse(activityPubContextify(activity, 'Announce', getContextFilter()), res)
}
async function videoAnnouncesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo
if (redirectIfNotOwned(video.url, res)) return
const handler = async (start: number, count: number) => {
const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
return {
total: result.total,
data: result.data.map(r => r.url)
}
}
const json = await activityPubCollectionPagination(getLocalVideoSharesActivityPubUrl(video), handler, req.query.page)
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
}
async function videoLikesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo
if (redirectIfNotOwned(video.url, res)) return
const json = await videoRates(req, 'like', video, getLocalVideoLikesActivityPubUrl(video))
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
}
async function videoDislikesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo
if (redirectIfNotOwned(video.url, res)) return
const json = await videoRates(req, 'dislike', video, getLocalVideoDislikesActivityPubUrl(video))
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
}
async function videoCommentsController (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo
if (redirectIfNotOwned(video.url, res)) return
const handler = async (start: number, count: number) => {
const result = await VideoCommentModel.listAndCountByVideoForAP({ video, start, count })
return {
total: result.total,
data: result.data.map(r => r.url)
}
}
const json = await activityPubCollectionPagination(getLocalVideoCommentsActivityPubUrl(video), handler, req.query.page)
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
}
async function videoChannelController (req: express.Request, res: express.Response) {
const videoChannel = res.locals.videoChannel
return activityPubResponse(activityPubContextify(await videoChannel.toActivityPubObject(), 'Actor', getContextFilter()), res)
}
async function videoChannelFollowersController (req: express.Request, res: express.Response) {
const videoChannel = res.locals.videoChannel
const activityPubResult = await actorFollowers(req, videoChannel.Actor)
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
}
async function videoChannelFollowingController (req: express.Request, res: express.Response) {
const videoChannel = res.locals.videoChannel
const activityPubResult = await actorFollowing(req, videoChannel.Actor)
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
}
async function videoCommentController (req: express.Request, res: express.Response) {
const videoComment = res.locals.videoCommentFull
if (redirectIfNotOwned(videoComment.url, res)) return
if (videoComment.Video.isOwned() && videoComment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
const threadParentComments = await VideoCommentModel.listThreadParentComments({ comment: videoComment })
const isPublic = true // Comments are always public
let videoCommentObject = videoComment.toActivityPubObject(threadParentComments)
if (videoComment.Account) {
const audience = getAudience(videoComment.Account.Actor, isPublic)
videoCommentObject = audiencify(videoCommentObject, audience)
if (req.path.endsWith('/activity')) {
const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject as VideoCommentObject, audience)
return activityPubResponse(activityPubContextify(data, 'Comment', getContextFilter()), res)
}
}
return activityPubResponse(activityPubContextify(videoCommentObject, 'Comment', getContextFilter()), res)
}
async function videoCommentApprovedController (req: express.Request, res: express.Response) {
const comment = res.locals.videoCommentFull
if (!comment.Video.isOwned() || comment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
const activity = buildApprovalActivity({ comment, type: 'ApproveReply' })
return activityPubResponse(activityPubContextify(activity, 'ApproveReply', getContextFilter()), res)
}
async function videoChaptersController (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo
if (redirectIfNotOwned(video.url, res)) return
const chapters = await VideoChapterModel.listChaptersOfVideo(video.id)
const chaptersObject: VideoChaptersObject = {
id: getLocalVideoChaptersActivityPubUrl(video),
hasPart: buildChaptersAPHasPart(video, chapters)
}
return activityPubResponse(activityPubContextify(chaptersObject, 'Chapters', getContextFilter()), res)
}
async function videoRedundancyController (req: express.Request, res: express.Response) {
const videoRedundancy = res.locals.videoRedundancy
if (redirectIfNotOwned(videoRedundancy.url, res)) return
const serverActor = await getServerActor()
const audience = getAudience(serverActor)
const object = audiencify(videoRedundancy.toActivityPubObject(), audience)
if (req.path.endsWith('/activity')) {
const data = buildCreateActivity(videoRedundancy.url, serverActor, object, audience)
return activityPubResponse(activityPubContextify(data, 'CacheFile', getContextFilter()), res)
}
return activityPubResponse(activityPubContextify(object, 'CacheFile', getContextFilter()), res)
}
async function videoPlaylistController (req: express.Request, res: express.Response) {
const playlist = res.locals.videoPlaylistFull
if (redirectIfNotOwned(playlist.url, res)) return
// We need more attributes
playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId)
const json = await playlist.toActivityPubObject(req.query.page, null)
const audience = getAudience(playlist.OwnerAccount.Actor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
const object = audiencify(json, audience)
return activityPubResponse(activityPubContextify(object, 'Playlist', getContextFilter()), res)
}
function videoPlaylistElementController (req: express.Request, res: express.Response) {
const videoPlaylistElement = res.locals.videoPlaylistElementAP
if (redirectIfNotOwned(videoPlaylistElement.url, res)) return
const json = videoPlaylistElement.toActivityPubObject()
return activityPubResponse(activityPubContextify(json, 'Playlist', getContextFilter()), res)
}
function getVideoLocalViewerController (req: express.Request, res: express.Response) {
const localViewer = res.locals.localViewerFull
return activityPubResponse(activityPubContextify(localViewer.toActivityPubObject(), 'WatchAction', getContextFilter()), res)
}
// ---------------------------------------------------------------------------
function actorFollowing (req: express.Request, actor: MActorId) {
const handler = (start: number, count: number) => {
return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
}
return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
}
function actorFollowers (req: express.Request, actor: MActorId) {
const handler = (start: number, count: number) => {
return ActorFollowModel.listAcceptedFollowerUrlsForAP([ actor.id ], undefined, start, count)
}
return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
}
function actorPlaylists (req: express.Request, options: { account: MAccountId } | { channel: MChannelId }) {
const handler = (start: number, count: number) => {
return VideoPlaylistModel.listPublicUrlsOfForAP(options, start, count)
}
return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
}
function videoRates (req: express.Request, rateType: VideoRateType, video: MVideoId, url: string) {
const handler = async (start: number, count: number) => {
const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
return {
total: result.total,
data: result.data.map(r => r.url)
}
}
return activityPubCollectionPagination(url, handler, req.query.page)
}
function redirectIfNotOwned (url: string, res: express.Response) {
if (url.startsWith(WEBSERVER.URL) === false) {
res.redirect(url)
return true
}
return false
}
+84
ファイルの表示
@@ -0,0 +1,84 @@
import express from 'express'
import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, HttpStatusCode, RootActivity } from '@peertube/peertube-models'
import { InboxManager } from '@server/lib/activitypub/inbox-manager.js'
import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity.js'
import { logger } from '../../helpers/logger.js'
import {
activityPubRateLimiter,
asyncMiddleware,
checkSignature,
ensureIsLocalChannel,
localAccountValidator,
signatureValidator,
videoChannelsNameWithHostValidator
} from '../../middlewares/index.js'
import { activityPubValidator } from '../../middlewares/validators/activitypub/activity.js'
const inboxRouter = express.Router()
inboxRouter.post('/inbox',
activityPubRateLimiter,
signatureValidator,
asyncMiddleware(checkSignature),
asyncMiddleware(activityPubValidator),
inboxController
)
inboxRouter.post('/accounts/:name/inbox',
activityPubRateLimiter,
signatureValidator,
asyncMiddleware(checkSignature),
asyncMiddleware(localAccountValidator),
asyncMiddleware(activityPubValidator),
inboxController
)
inboxRouter.post('/video-channels/:nameWithHost/inbox',
activityPubRateLimiter,
signatureValidator,
asyncMiddleware(checkSignature),
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(activityPubValidator),
inboxController
)
// ---------------------------------------------------------------------------
export {
inboxRouter
}
// ---------------------------------------------------------------------------
function inboxController (req: express.Request, res: express.Response) {
const rootActivity: RootActivity = req.body
let activities: Activity[]
if ([ 'Collection', 'CollectionPage' ].includes(rootActivity.type)) {
activities = (rootActivity as ActivityPubCollection).items
} else if ([ 'OrderedCollection', 'OrderedCollectionPage' ].includes(rootActivity.type)) {
activities = (rootActivity as ActivityPubOrderedCollection<Activity>).orderedItems
} else {
activities = [ rootActivity as Activity ]
}
// Only keep activities we are able to process
logger.debug('Filtering %d activities...', activities.length, { activities })
activities = activities.filter(a => isActivityValid(a))
logger.debug('We keep %d activities.', activities.length, { activities })
const accountOrChannel = res.locals.account || res.locals.videoChannel
logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor.url)
InboxManager.Instance.addInboxMessage({
activities,
signatureActor: res.locals.signature.actor,
inboxActor: accountOrChannel
? accountOrChannel.Actor
: undefined
})
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
+17
ファイルの表示
@@ -0,0 +1,17 @@
import express from 'express'
import { activityPubClientRouter } from './client.js'
import { inboxRouter } from './inbox.js'
import { outboxRouter } from './outbox.js'
const activityPubRouter = express.Router()
activityPubRouter.use('/', inboxRouter)
activityPubRouter.use('/', outboxRouter)
activityPubRouter.use('/', activityPubClientRouter)
// ---------------------------------------------------------------------------
export {
activityPubRouter
}
+86
ファイルの表示
@@ -0,0 +1,86 @@
import express from 'express'
import { Activity, VideoPrivacy } from '@peertube/peertube-models'
import { activityPubContextify } from '@server/helpers/activity-pub-utils.js'
import { activityPubCollectionPagination } from '@server/lib/activitypub/collection.js'
import { getContextFilter } from '@server/lib/activitypub/context.js'
import { MActorLight } from '@server/types/models/index.js'
import { logger } from '../../helpers/logger.js'
import { buildAudience } from '../../lib/activitypub/audience.js'
import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send/index.js'
import {
activityPubRateLimiter,
asyncMiddleware,
ensureIsLocalChannel,
localAccountValidator,
videoChannelsNameWithHostValidator
} from '../../middlewares/index.js'
import { apPaginationValidator } from '../../middlewares/validators/activitypub/index.js'
import { VideoModel } from '../../models/video/video.js'
import { activityPubResponse } from './utils.js'
const outboxRouter = express.Router()
outboxRouter.get('/accounts/:name/outbox',
activityPubRateLimiter,
apPaginationValidator,
localAccountValidator,
asyncMiddleware(outboxController)
)
outboxRouter.get('/video-channels/:nameWithHost/outbox',
activityPubRateLimiter,
apPaginationValidator,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(outboxController)
)
// ---------------------------------------------------------------------------
export {
outboxRouter
}
// ---------------------------------------------------------------------------
async function outboxController (req: express.Request, res: express.Response) {
const accountOrVideoChannel = res.locals.account || res.locals.videoChannel
const actor = accountOrVideoChannel.Actor
const actorOutboxUrl = actor.url + '/outbox'
logger.info('Receiving outbox request for %s.', actorOutboxUrl)
const handler = (start: number, count: number) => buildActivities(actor, start, count)
const json = await activityPubCollectionPagination(actorOutboxUrl, handler, req.query.page, req.query.size)
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
}
async function buildActivities (actor: MActorLight, start: number, count: number) {
const data = await VideoModel.listAllAndSharedByActorForOutbox(actor.id, start, count)
const activities: Activity[] = []
for (const video of data.data) {
const byActor = video.VideoChannel.Account.Actor
const createActivityAudience = buildAudience([ byActor.followersUrl ], video.privacy === VideoPrivacy.PUBLIC)
// This is a shared video
if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
const videoShare = video.VideoShares[0]
const announceActivity = buildAnnounceActivity(videoShare.url, actor, video.url, createActivityAudience)
activities.push(announceActivity)
} else {
// FIXME: only use the video URL to reduce load. Breaks compat with PeerTube < 6.0.0
const videoObject = await video.toActivityPubObject()
const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience)
activities.push(createActivity)
}
}
return {
data: activities,
total: data.total
}
}
+12
ファイルの表示
@@ -0,0 +1,12 @@
import express from 'express'
async function activityPubResponse (promise: Promise<any>, res: express.Response) {
const data = await promise
return res.type('application/activity+json; charset=utf-8')
.json(data)
}
export {
activityPubResponse
}
+270
ファイルの表示
@@ -0,0 +1,270 @@
import express from 'express'
import { logger } from '@server/helpers/logger.js'
import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation.js'
import { Notifier } from '@server/lib/notifier/index.js'
import { AbuseMessageModel } from '@server/models/abuse/abuse-message.js'
import { AbuseModel } from '@server/models/abuse/abuse.js'
import { getServerActor } from '@server/models/application/application.js'
import { abusePredefinedReasonsMap } from '@peertube/peertube-core-utils'
import { AbuseCreate, AbuseState, HttpStatusCode, UserRight } from '@peertube/peertube-models'
import { getFormattedObjects } from '../../helpers/utils.js'
import { sequelizeTypescript } from '../../initializers/database.js'
import {
abuseGetValidator,
abuseListForAdminsValidator,
abuseReportValidator,
abusesSortValidator,
abuseUpdateValidator,
addAbuseMessageValidator,
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
checkAbuseValidForMessagesValidator,
deleteAbuseMessageValidator,
ensureUserHasRight,
getAbuseValidator,
openapiOperationDoc,
paginationValidator,
setDefaultPagination,
setDefaultSort
} from '../../middlewares/index.js'
import { AccountModel } from '../../models/account/account.js'
const abuseRouter = express.Router()
abuseRouter.use(apiRateLimiter)
abuseRouter.get('/',
openapiOperationDoc({ operationId: 'getAbuses' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_ABUSES),
paginationValidator,
abusesSortValidator,
setDefaultSort,
setDefaultPagination,
abuseListForAdminsValidator,
asyncMiddleware(listAbusesForAdmins)
)
abuseRouter.put('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_ABUSES),
asyncMiddleware(abuseUpdateValidator),
asyncRetryTransactionMiddleware(updateAbuse)
)
abuseRouter.post('/',
authenticate,
asyncMiddleware(abuseReportValidator),
asyncRetryTransactionMiddleware(reportAbuse)
)
abuseRouter.delete('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_ABUSES),
asyncMiddleware(abuseGetValidator),
asyncRetryTransactionMiddleware(deleteAbuse)
)
abuseRouter.get('/:id/messages',
authenticate,
asyncMiddleware(getAbuseValidator),
checkAbuseValidForMessagesValidator,
asyncRetryTransactionMiddleware(listAbuseMessages)
)
abuseRouter.post('/:id/messages',
authenticate,
asyncMiddleware(getAbuseValidator),
checkAbuseValidForMessagesValidator,
addAbuseMessageValidator,
asyncRetryTransactionMiddleware(addAbuseMessage)
)
abuseRouter.delete('/:id/messages/:messageId',
authenticate,
asyncMiddleware(getAbuseValidator),
checkAbuseValidForMessagesValidator,
asyncMiddleware(deleteAbuseMessageValidator),
asyncRetryTransactionMiddleware(deleteAbuseMessage)
)
// ---------------------------------------------------------------------------
export {
abuseRouter
}
// ---------------------------------------------------------------------------
async function listAbusesForAdmins (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user
const serverActor = await getServerActor()
const resultList = await AbuseModel.listForAdminApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
id: req.query.id,
filter: req.query.filter,
predefinedReason: req.query.predefinedReason,
search: req.query.search,
state: req.query.state,
videoIs: req.query.videoIs,
searchReporter: req.query.searchReporter,
searchReportee: req.query.searchReportee,
searchVideo: req.query.searchVideo,
searchVideoChannel: req.query.searchVideoChannel,
serverAccountId: serverActor.Account.id,
user
})
return res.json({
total: resultList.total,
data: resultList.data.map(d => d.toFormattedAdminJSON())
})
}
async function updateAbuse (req: express.Request, res: express.Response) {
const abuse = res.locals.abuse
let stateUpdated = false
if (req.body.moderationComment !== undefined) abuse.moderationComment = req.body.moderationComment
if (req.body.state !== undefined) {
abuse.state = req.body.state
// We consider the abuse has been processed when its state change
if (!abuse.processedAt) abuse.processedAt = new Date()
stateUpdated = true
}
await sequelizeTypescript.transaction(t => {
return abuse.save({ transaction: t })
})
if (stateUpdated === true) {
AbuseModel.loadFull(abuse.id)
.then(abuseFull => Notifier.Instance.notifyOnAbuseStateChange(abuseFull))
.catch(err => logger.error('Cannot notify on abuse state change', { err }))
}
// Do not send the delete to other instances, we updated OUR copy of this abuse
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function deleteAbuse (req: express.Request, res: express.Response) {
const abuse = res.locals.abuse
await sequelizeTypescript.transaction(t => {
return abuse.destroy({ transaction: t })
})
// Do not send the delete to other instances, we delete OUR copy of this abuse
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function reportAbuse (req: express.Request, res: express.Response) {
const videoInstance = res.locals.videoAll
const commentInstance = res.locals.videoCommentFull
const accountInstance = res.locals.account
const body: AbuseCreate = req.body
const { id } = await sequelizeTypescript.transaction(async t => {
const user = res.locals.oauth.token.User
// Don't send abuse notification if reporter is an admin/moderator
const skipNotification = user.hasRight(UserRight.MANAGE_ABUSES)
const reporterAccount = await AccountModel.load(user.Account.id, t)
const predefinedReasons = body.predefinedReasons?.map(r => abusePredefinedReasonsMap[r])
const baseAbuse = {
reporterAccountId: reporterAccount.id,
reason: body.reason,
state: AbuseState.PENDING,
predefinedReasons
}
if (body.video) {
return createVideoAbuse({
baseAbuse,
videoInstance,
reporterAccount,
transaction: t,
startAt: body.video.startAt,
endAt: body.video.endAt,
skipNotification
})
}
if (body.comment) {
return createVideoCommentAbuse({
baseAbuse,
commentInstance,
reporterAccount,
transaction: t,
skipNotification
})
}
// Account report
return createAccountAbuse({
baseAbuse,
accountInstance,
reporterAccount,
transaction: t,
skipNotification
})
})
return res.json({ abuse: { id } })
}
async function listAbuseMessages (req: express.Request, res: express.Response) {
const abuse = res.locals.abuse
const resultList = await AbuseMessageModel.listForApi(abuse.id)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function addAbuseMessage (req: express.Request, res: express.Response) {
const abuse = res.locals.abuse
const user = res.locals.oauth.token.user
const byModerator = abuse.reporterAccountId !== user.Account.id
const abuseMessage = await AbuseMessageModel.create({
message: req.body.message,
byModerator,
accountId: user.Account.id,
abuseId: abuse.id
})
// If a moderator created an abuse message, we consider it as processed
if (byModerator && !abuse.processedAt) {
abuse.processedAt = new Date()
await abuse.save()
}
AbuseModel.loadFull(abuse.id)
.then(abuseFull => Notifier.Instance.notifyOnAbuseMessage(abuseFull, abuseMessage))
.catch(err => logger.error('Cannot notify on new abuse message', { err }))
return res.json({
abuseMessage: {
id: abuseMessage.id
}
})
}
async function deleteAbuseMessage (req: express.Request, res: express.Response) {
const abuseMessage = res.locals.abuseMessage
await sequelizeTypescript.transaction(t => {
return abuseMessage.destroy({ transaction: t })
})
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
+270
ファイルの表示
@@ -0,0 +1,270 @@
import express from 'express'
import { pickCommonVideoQuery } from '@server/helpers/query.js'
import { ActorFollowModel } from '@server/models/actor/actor-follow.js'
import { getServerActor } from '@server/models/application/application.js'
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils.js'
import { getFormattedObjects } from '../../helpers/utils.js'
import { JobQueue } from '../../lib/job-queue/index.js'
import { Hooks } from '../../lib/plugins/hooks.js'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
commonVideosFiltersValidator,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort,
setDefaultVideosSort,
videoPlaylistsSortValidator,
videoRatesSortValidator,
videoRatingValidator
} from '../../middlewares/index.js'
import {
accountNameWithHostGetValidator,
accountsFollowersSortValidator,
accountsSortValidator,
ensureAuthUserOwnsAccountValidator,
ensureCanManageChannelOrAccount,
videoChannelsSortValidator,
videoChannelStatsValidator,
videoChannelSyncsSortValidator,
videosSortValidator
} from '../../middlewares/validators/index.js'
import { commonVideoPlaylistFiltersValidator, videoPlaylistsSearchValidator } from '../../middlewares/validators/videos/video-playlists.js'
import { AccountModel } from '../../models/account/account.js'
import { AccountVideoRateModel } from '../../models/account/account-video-rate.js'
import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter/index.js'
import { VideoModel } from '../../models/video/video.js'
import { VideoChannelModel } from '../../models/video/video-channel.js'
import { VideoPlaylistModel } from '../../models/video/video-playlist.js'
const accountsRouter = express.Router()
accountsRouter.use(apiRateLimiter)
accountsRouter.get('/',
paginationValidator,
accountsSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listAccounts)
)
accountsRouter.get('/:accountName',
asyncMiddleware(accountNameWithHostGetValidator),
getAccount
)
accountsRouter.get('/:accountName/videos',
asyncMiddleware(accountNameWithHostGetValidator),
paginationValidator,
videosSortValidator,
setDefaultVideosSort,
setDefaultPagination,
optionalAuthenticate,
commonVideosFiltersValidator,
asyncMiddleware(listAccountVideos)
)
accountsRouter.get('/:accountName/video-channels',
asyncMiddleware(accountNameWithHostGetValidator),
videoChannelStatsValidator,
paginationValidator,
videoChannelsSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listAccountChannels)
)
accountsRouter.get('/:accountName/video-channel-syncs',
authenticate,
asyncMiddleware(accountNameWithHostGetValidator),
ensureCanManageChannelOrAccount,
paginationValidator,
videoChannelSyncsSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listAccountChannelsSync)
)
accountsRouter.get('/:accountName/video-playlists',
optionalAuthenticate,
asyncMiddleware(accountNameWithHostGetValidator),
paginationValidator,
videoPlaylistsSortValidator,
setDefaultSort,
setDefaultPagination,
commonVideoPlaylistFiltersValidator,
videoPlaylistsSearchValidator,
asyncMiddleware(listAccountPlaylists)
)
accountsRouter.get('/:accountName/ratings',
authenticate,
asyncMiddleware(accountNameWithHostGetValidator),
ensureAuthUserOwnsAccountValidator,
paginationValidator,
videoRatesSortValidator,
setDefaultSort,
setDefaultPagination,
videoRatingValidator,
asyncMiddleware(listAccountRatings)
)
accountsRouter.get('/:accountName/followers',
authenticate,
asyncMiddleware(accountNameWithHostGetValidator),
ensureAuthUserOwnsAccountValidator,
paginationValidator,
accountsFollowersSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listAccountFollowers)
)
// ---------------------------------------------------------------------------
export {
accountsRouter
}
// ---------------------------------------------------------------------------
function getAccount (req: express.Request, res: express.Response) {
const account = res.locals.account
if (account.isOutdated()) {
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'actor', url: account.Actor.url } })
}
return res.json(account.toFormattedJSON())
}
async function listAccounts (req: express.Request, res: express.Response) {
const resultList = await AccountModel.listForApi(req.query.start, req.query.count, req.query.sort)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function listAccountChannels (req: express.Request, res: express.Response) {
const options = {
accountId: res.locals.account.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
withStats: req.query.withStats,
search: req.query.search
}
const resultList = await VideoChannelModel.listByAccountForAPI(options)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function listAccountChannelsSync (req: express.Request, res: express.Response) {
const options = {
accountId: res.locals.account.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search
}
const resultList = await VideoChannelSyncModel.listByAccountForAPI(options)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function listAccountPlaylists (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
// Allow users to see their private/unlisted video playlists
let listMyPlaylists = false
if (res.locals.oauth && res.locals.oauth.token.User.Account.id === res.locals.account.id) {
listMyPlaylists = true
}
const resultList = await VideoPlaylistModel.listForApi({
search: req.query.search,
followerActorId: isUserAbleToSearchRemoteURI(res)
? null
: serverActor.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
accountId: res.locals.account.id,
listMyPlaylists,
type: req.query.playlistType
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function listAccountVideos (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const account = res.locals.account
const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res)
? null
: {
actorId: serverActor.id,
orLocalVideos: true
}
const countVideos = getCountVideos(req)
const query = pickCommonVideoQuery(req.query)
const apiOptions = await Hooks.wrapObject({
...query,
displayOnlyForFollower,
nsfw: buildNSFWFilter(res, query.nsfw),
accountId: account.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos
}, 'filter:api.accounts.videos.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoModel.listForApi.bind(VideoModel),
apiOptions,
'filter:api.accounts.videos.list.result'
)
return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
}
async function listAccountRatings (req: express.Request, res: express.Response) {
const account = res.locals.account
const resultList = await AccountVideoRateModel.listByAccountForApi({
accountId: account.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
type: req.query.rating
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function listAccountFollowers (req: express.Request, res: express.Response) {
const account = res.locals.account
const channels = await VideoChannelModel.listAllByAccount(account.id)
const actorIds = [ account.actorId ].concat(channels.map(c => c.actorId))
const resultList = await ActorFollowModel.listFollowersForApi({
actorIds,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
state: 'accepted'
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
+82
ファイルの表示
@@ -0,0 +1,82 @@
import { AutomaticTagPolicy, CommentAutomaticTagPoliciesUpdate, HttpStatusCode, UserRight } from '@peertube/peertube-models'
import { AutomaticTagger } from '@server/lib/automatic-tags/automatic-tagger.js'
import { setAccountAutomaticTagsPolicy } from '@server/lib/automatic-tags/automatic-tags.js'
import {
manageAccountAutomaticTagsValidator,
updateAutomaticTagPoliciesValidator
} from '@server/middlewares/validators/automatic-tags.js'
import { getServerActor } from '@server/models/application/application.js'
import express from 'express'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
ensureUserHasRight
} from '../../middlewares/index.js'
const automaticTagRouter = express.Router()
automaticTagRouter.use(apiRateLimiter)
automaticTagRouter.get('/policies/accounts/:accountName/comments',
authenticate,
asyncMiddleware(manageAccountAutomaticTagsValidator),
asyncMiddleware(getAutomaticTagPolicies)
)
automaticTagRouter.put('/policies/accounts/:accountName/comments',
authenticate,
asyncMiddleware(manageAccountAutomaticTagsValidator),
asyncMiddleware(updateAutomaticTagPoliciesValidator),
asyncMiddleware(updateAutomaticTagPolicies)
)
// ---------------------------------------------------------------------------
automaticTagRouter.get('/accounts/:accountName/available',
authenticate,
asyncMiddleware(manageAccountAutomaticTagsValidator),
asyncMiddleware(getAccountAutomaticTagAvailable)
)
automaticTagRouter.get('/server/available',
authenticate,
ensureUserHasRight(UserRight.MANAGE_INSTANCE_AUTO_TAGS),
asyncMiddleware(getServerAutomaticTagAvailable)
)
// ---------------------------------------------------------------------------
export {
automaticTagRouter
}
// ---------------------------------------------------------------------------
async function getAutomaticTagPolicies (req: express.Request, res: express.Response) {
const result = await AutomaticTagger.getAutomaticTagPolicies(res.locals.account)
return res.json(result)
}
async function updateAutomaticTagPolicies (req: express.Request, res: express.Response) {
await setAccountAutomaticTagsPolicy({
account: res.locals.account,
policy: AutomaticTagPolicy.REVIEW_COMMENT,
tags: (req.body as CommentAutomaticTagPoliciesUpdate).review
})
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function getAccountAutomaticTagAvailable (req: express.Request, res: express.Response) {
const result = await AutomaticTagger.getAutomaticTagAvailable(res.locals.account)
return res.json(result)
}
async function getServerAutomaticTagAvailable (req: express.Request, res: express.Response) {
const result = await AutomaticTagger.getAutomaticTagAvailable((await getServerActor()).Account)
return res.json(result)
}
+110
ファイルの表示
@@ -0,0 +1,110 @@
import express from 'express'
import { handleToNameAndHost } from '@server/helpers/actors.js'
import { logger } from '@server/helpers/logger.js'
import { AccountBlocklistModel } from '@server/models/account/account-blocklist.js'
import { getServerActor } from '@server/models/application/application.js'
import { ServerBlocklistModel } from '@server/models/server/server-blocklist.js'
import { MActorAccountId, MUserAccountId } from '@server/types/models/index.js'
import { BlockStatus } from '@peertube/peertube-models'
import { apiRateLimiter, asyncMiddleware, blocklistStatusValidator, optionalAuthenticate } from '../../middlewares/index.js'
const blocklistRouter = express.Router()
blocklistRouter.use(apiRateLimiter)
blocklistRouter.get('/status',
optionalAuthenticate,
blocklistStatusValidator,
asyncMiddleware(getBlocklistStatus)
)
// ---------------------------------------------------------------------------
export {
blocklistRouter
}
// ---------------------------------------------------------------------------
async function getBlocklistStatus (req: express.Request, res: express.Response) {
const hosts = req.query.hosts as string[]
const accounts = req.query.accounts as string[]
const user = res.locals.oauth?.token.User
const serverActor = await getServerActor()
const byAccountIds = [ serverActor.Account.id ]
if (user) byAccountIds.push(user.Account.id)
const status: BlockStatus = {
accounts: {},
hosts: {}
}
const baseOptions = {
byAccountIds,
user,
serverActor,
status
}
await Promise.all([
populateServerBlocklistStatus({ ...baseOptions, hosts }),
populateAccountBlocklistStatus({ ...baseOptions, accounts })
])
return res.json(status)
}
async function populateServerBlocklistStatus (options: {
byAccountIds: number[]
user?: MUserAccountId
serverActor: MActorAccountId
hosts: string[]
status: BlockStatus
}) {
const { byAccountIds, user, serverActor, hosts, status } = options
if (!hosts || hosts.length === 0) return
const serverBlocklistStatus = await ServerBlocklistModel.getBlockStatus(byAccountIds, hosts)
logger.debug('Got server blocklist status.', { serverBlocklistStatus, byAccountIds, hosts })
for (const host of hosts) {
const block = serverBlocklistStatus.find(b => b.host === host)
status.hosts[host] = getStatus(block, serverActor, user)
}
}
async function populateAccountBlocklistStatus (options: {
byAccountIds: number[]
user?: MUserAccountId
serverActor: MActorAccountId
accounts: string[]
status: BlockStatus
}) {
const { byAccountIds, user, serverActor, accounts, status } = options
if (!accounts || accounts.length === 0) return
const accountBlocklistStatus = await AccountBlocklistModel.getBlockStatus(byAccountIds, accounts)
logger.debug('Got account blocklist status.', { accountBlocklistStatus, byAccountIds, accounts })
for (const account of accounts) {
const sanitizedHandle = handleToNameAndHost(account)
const block = accountBlocklistStatus.find(b => b.name === sanitizedHandle.name && b.host === sanitizedHandle.host)
status.accounts[sanitizedHandle.handle] = getStatus(block, serverActor, user)
}
}
function getStatus (block: { accountId: number }, serverActor: MActorAccountId, user?: MUserAccountId) {
return {
blockedByServer: !!(block && block.accountId === serverActor.Account.id),
blockedByUser: !!(block && user && block.accountId === user.Account.id)
}
}
+43
ファイルの表示
@@ -0,0 +1,43 @@
import express from 'express'
import { BulkRemoveCommentsOfBody, HttpStatusCode } from '@peertube/peertube-models'
import { removeComment } from '@server/lib/video-comment.js'
import { bulkRemoveCommentsOfValidator } from '@server/middlewares/validators/bulk.js'
import { VideoCommentModel } from '@server/models/video/video-comment.js'
import { apiRateLimiter, asyncMiddleware, authenticate } from '../../middlewares/index.js'
const bulkRouter = express.Router()
bulkRouter.use(apiRateLimiter)
bulkRouter.post('/remove-comments-of',
authenticate,
asyncMiddleware(bulkRemoveCommentsOfValidator),
asyncMiddleware(bulkRemoveCommentsOf)
)
// ---------------------------------------------------------------------------
export {
bulkRouter
}
// ---------------------------------------------------------------------------
async function bulkRemoveCommentsOf (req: express.Request, res: express.Response) {
const account = res.locals.account
const body = req.body as BulkRemoveCommentsOfBody
const user = res.locals.oauth.token.User
const filter = body.scope === 'my-videos'
? { onVideosOfAccount: user.Account }
: {}
const comments = await VideoCommentModel.listForBulkDelete(account, filter)
// Don't wait result
res.status(HttpStatusCode.NO_CONTENT_204).end()
for (const comment of comments) {
await removeComment(comment, req, res)
}
}
+497
ファイルの表示
@@ -0,0 +1,497 @@
import { About, ActorImageType, ActorImageType_Type, CustomConfig, HttpStatusCode, UserRight } from '@peertube/peertube-models'
import { createReqFiles } from '@server/helpers/express-utils.js'
import { MIMETYPES } from '@server/initializers/constants.js'
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '@server/lib/local-actor.js'
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
import { ActorImageModel } from '@server/models/actor/actor-image.js'
import { getServerActor } from '@server/models/application/application.js'
import { ModelCache } from '@server/models/shared/model-cache.js'
import express from 'express'
import { remove, writeJSON } from 'fs-extra/esm'
import snakeCase from 'lodash-es/snakeCase.js'
import validator from 'validator'
import { CustomConfigAuditView, auditLoggerFactory, getAuditIdFromRes } from '../../helpers/audit-logger.js'
import { objectConverter } from '../../helpers/core-utils.js'
import { CONFIG, reloadConfig } from '../../initializers/config.js'
import { ClientHtml } from '../../lib/html/client-html.js'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
ensureUserHasRight,
openapiOperationDoc,
updateAvatarValidator,
updateBannerValidator
} from '../../middlewares/index.js'
import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config.js'
const configRouter = express.Router()
configRouter.use(apiRateLimiter)
const auditLogger = auditLoggerFactory('config')
configRouter.get('/',
openapiOperationDoc({ operationId: 'getConfig' }),
asyncMiddleware(getConfig)
)
configRouter.get('/about',
openapiOperationDoc({ operationId: 'getAbout' }),
asyncMiddleware(getAbout)
)
configRouter.get('/custom',
openapiOperationDoc({ operationId: 'getCustomConfig' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
getCustomConfig
)
configRouter.put('/custom',
openapiOperationDoc({ operationId: 'putCustomConfig' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
ensureConfigIsEditable,
customConfigUpdateValidator,
asyncMiddleware(updateCustomConfig)
)
configRouter.delete('/custom',
openapiOperationDoc({ operationId: 'delCustomConfig' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
ensureConfigIsEditable,
asyncMiddleware(deleteCustomConfig)
)
// ---------------------------------------------------------------------------
configRouter.post('/instance-banner/pick',
authenticate,
createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT),
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
updateBannerValidator,
asyncMiddleware(updateInstanceImageFactory(ActorImageType.BANNER))
)
configRouter.delete('/instance-banner',
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
asyncMiddleware(deleteInstanceImageFactory(ActorImageType.BANNER))
)
// ---------------------------------------------------------------------------
configRouter.post('/instance-avatar/pick',
authenticate,
createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT),
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
updateAvatarValidator,
asyncMiddleware(updateInstanceImageFactory(ActorImageType.AVATAR))
)
configRouter.delete('/instance-avatar',
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
asyncMiddleware(deleteInstanceImageFactory(ActorImageType.AVATAR))
)
// ---------------------------------------------------------------------------
async function getConfig (req: express.Request, res: express.Response) {
const json = await ServerConfigManager.Instance.getServerConfig(req.ip)
return res.json(json)
}
async function getAbout (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const about: About = {
instance: {
name: CONFIG.INSTANCE.NAME,
shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
description: CONFIG.INSTANCE.DESCRIPTION,
terms: CONFIG.INSTANCE.TERMS,
codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
creationReason: CONFIG.INSTANCE.CREATION_REASON,
moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
administrator: CONFIG.INSTANCE.ADMINISTRATOR,
maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
languages: CONFIG.INSTANCE.LANGUAGES,
categories: CONFIG.INSTANCE.CATEGORIES,
banners: serverActor.Banners.map(b => b.toFormattedJSON()),
avatars: serverActor.Avatars.map(a => a.toFormattedJSON())
}
}
return res.json(about)
}
function getCustomConfig (req: express.Request, res: express.Response) {
const data = customConfig()
return res.json(data)
}
async function deleteCustomConfig (req: express.Request, res: express.Response) {
await remove(CONFIG.CUSTOM_FILE)
auditLogger.delete(getAuditIdFromRes(res), new CustomConfigAuditView(customConfig()))
await reloadConfig()
ClientHtml.invalidateCache()
const data = customConfig()
return res.json(data)
}
async function updateCustomConfig (req: express.Request, res: express.Response) {
const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
// camelCase to snake_case key + Force number conversion
const toUpdateJSON = convertCustomConfigBody(req.body)
await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
await reloadConfig()
ClientHtml.invalidateCache()
const data = customConfig()
auditLogger.update(
getAuditIdFromRes(res),
new CustomConfigAuditView(data),
oldCustomConfigAuditKeys
)
return res.json(data)
}
// ---------------------------------------------------------------------------
function updateInstanceImageFactory (imageType: ActorImageType_Type) {
return async (req: express.Request, res: express.Response) => {
const field = imageType === ActorImageType.BANNER
? 'bannerfile'
: 'avatarfile'
const imagePhysicalFile = req.files[field][0]
await updateLocalActorImageFiles({
accountOrChannel: (await getServerActorWithUpdatedImages(imageType)).Account,
imagePhysicalFile,
type: imageType,
sendActorUpdate: false
})
ClientHtml.invalidateCache()
ModelCache.Instance.clearCache('server-account')
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
}
function deleteInstanceImageFactory (imageType: ActorImageType_Type) {
return async (req: express.Request, res: express.Response) => {
await deleteLocalActorImageFile((await getServerActorWithUpdatedImages(imageType)).Account, imageType)
ClientHtml.invalidateCache()
ModelCache.Instance.clearCache('server-account')
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
}
async function getServerActorWithUpdatedImages (imageType: ActorImageType_Type) {
const serverActor = await getServerActor()
const updatedImages = await ActorImageModel.listByActor(serverActor, imageType) // Reload images from DB
if (imageType === ActorImageType.BANNER) serverActor.Banners = updatedImages
else serverActor.Avatars = updatedImages
return serverActor
}
// ---------------------------------------------------------------------------
export {
configRouter
}
// ---------------------------------------------------------------------------
function customConfig (): CustomConfig {
return {
instance: {
name: CONFIG.INSTANCE.NAME,
shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
description: CONFIG.INSTANCE.DESCRIPTION,
terms: CONFIG.INSTANCE.TERMS,
codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
creationReason: CONFIG.INSTANCE.CREATION_REASON,
moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
administrator: CONFIG.INSTANCE.ADMINISTRATOR,
maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
languages: CONFIG.INSTANCE.LANGUAGES,
categories: CONFIG.INSTANCE.CATEGORIES,
isNSFW: CONFIG.INSTANCE.IS_NSFW,
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
customizations: {
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
}
},
theme: {
default: CONFIG.THEME.DEFAULT
},
services: {
twitter: {
username: CONFIG.SERVICES.TWITTER.USERNAME
}
},
client: {
videos: {
miniature: {
preferAuthorDisplayName: CONFIG.CLIENT.VIDEOS.MINIATURE.PREFER_AUTHOR_DISPLAY_NAME
}
},
menu: {
login: {
redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
}
}
},
cache: {
previews: {
size: CONFIG.CACHE.PREVIEWS.SIZE
},
captions: {
size: CONFIG.CACHE.VIDEO_CAPTIONS.SIZE
},
torrents: {
size: CONFIG.CACHE.TORRENTS.SIZE
},
storyboards: {
size: CONFIG.CACHE.STORYBOARDS.SIZE
}
},
signup: {
enabled: CONFIG.SIGNUP.ENABLED,
limit: CONFIG.SIGNUP.LIMIT,
requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL,
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION,
minimumAge: CONFIG.SIGNUP.MINIMUM_AGE
},
admin: {
email: CONFIG.ADMIN.EMAIL
},
contactForm: {
enabled: CONFIG.CONTACT_FORM.ENABLED
},
user: {
history: {
videos: {
enabled: CONFIG.USER.HISTORY.VIDEOS.ENABLED
}
},
videoQuota: CONFIG.USER.VIDEO_QUOTA,
videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY,
defaultChannelName: CONFIG.USER.DEFAULT_CHANNEL_NAME
},
videoChannels: {
maxPerUser: CONFIG.VIDEO_CHANNELS.MAX_PER_USER
},
transcoding: {
enabled: CONFIG.TRANSCODING.ENABLED,
originalFile: {
keep: CONFIG.TRANSCODING.ORIGINAL_FILE.KEEP
},
remoteRunners: {
enabled: CONFIG.TRANSCODING.REMOTE_RUNNERS.ENABLED
},
allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
threads: CONFIG.TRANSCODING.THREADS,
concurrency: CONFIG.TRANSCODING.CONCURRENCY,
profile: CONFIG.TRANSCODING.PROFILE,
resolutions: {
'0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
'144p': CONFIG.TRANSCODING.RESOLUTIONS['144p'],
'240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'],
'360p': CONFIG.TRANSCODING.RESOLUTIONS['360p'],
'480p': CONFIG.TRANSCODING.RESOLUTIONS['480p'],
'720p': CONFIG.TRANSCODING.RESOLUTIONS['720p'],
'1080p': CONFIG.TRANSCODING.RESOLUTIONS['1080p'],
'1440p': CONFIG.TRANSCODING.RESOLUTIONS['1440p'],
'2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p']
},
alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION,
webVideos: {
enabled: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED
},
hls: {
enabled: CONFIG.TRANSCODING.HLS.ENABLED
}
},
live: {
enabled: CONFIG.LIVE.ENABLED,
allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
latencySetting: {
enabled: CONFIG.LIVE.LATENCY_SETTING.ENABLED
},
maxDuration: CONFIG.LIVE.MAX_DURATION,
maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
transcoding: {
enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
remoteRunners: {
enabled: CONFIG.LIVE.TRANSCODING.REMOTE_RUNNERS.ENABLED
},
threads: CONFIG.LIVE.TRANSCODING.THREADS,
profile: CONFIG.LIVE.TRANSCODING.PROFILE,
resolutions: {
'144p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['144p'],
'240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'],
'360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'],
'480p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['480p'],
'720p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['720p'],
'1080p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1080p'],
'1440p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1440p'],
'2160p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['2160p']
},
alwaysTranscodeOriginalResolution: CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION
}
},
videoStudio: {
enabled: CONFIG.VIDEO_STUDIO.ENABLED,
remoteRunners: {
enabled: CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED
}
},
videoTranscription: {
enabled: CONFIG.VIDEO_TRANSCRIPTION.ENABLED,
remoteRunners: {
enabled: CONFIG.VIDEO_TRANSCRIPTION.REMOTE_RUNNERS.ENABLED
}
},
videoFile: {
update: {
enabled: CONFIG.VIDEO_FILE.UPDATE.ENABLED
}
},
import: {
videos: {
concurrency: CONFIG.IMPORT.VIDEOS.CONCURRENCY,
http: {
enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
},
torrent: {
enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
}
},
videoChannelSynchronization: {
enabled: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED,
maxPerUser: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.MAX_PER_USER
},
users: {
enabled: CONFIG.IMPORT.USERS.ENABLED
}
},
export: {
users: {
enabled: CONFIG.EXPORT.USERS.ENABLED,
exportExpiration: CONFIG.EXPORT.USERS.EXPORT_EXPIRATION,
maxUserVideoQuota: CONFIG.EXPORT.USERS.MAX_USER_VIDEO_QUOTA
}
},
trending: {
videos: {
algorithms: {
enabled: CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED,
default: CONFIG.TRENDING.VIDEOS.ALGORITHMS.DEFAULT
}
}
},
autoBlacklist: {
videos: {
ofUsers: {
enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
}
}
},
followers: {
instance: {
enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED,
manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
}
},
followings: {
instance: {
autoFollowBack: {
enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED
},
autoFollowIndex: {
enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED,
indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
}
}
},
broadcastMessage: {
enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
level: CONFIG.BROADCAST_MESSAGE.LEVEL,
dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
},
search: {
remoteUri: {
users: CONFIG.SEARCH.REMOTE_URI.USERS,
anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
},
searchIndex: {
enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED,
url: CONFIG.SEARCH.SEARCH_INDEX.URL,
disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH,
isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH
}
},
storyboards: {
enabled: CONFIG.STORYBOARDS.ENABLED
}
}
}
function convertCustomConfigBody (body: CustomConfig) {
function keyConverter (k: string) {
// Transcoding resolutions exception
if (/^\d{3,4}p$/.exec(k)) return k
if (k === '0p') return k
return snakeCase(k)
}
function valueConverter (v: any) {
if (validator.default.isNumeric(v + '')) return parseInt('' + v, 10)
return v
}
return objectConverter(body, keyConverter, valueConverter)
}
+48
ファイルの表示
@@ -0,0 +1,48 @@
import express from 'express'
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
import { ActorCustomPageModel } from '@server/models/account/actor-custom-page.js'
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
import { apiRateLimiter, asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares/index.js'
const customPageRouter = express.Router()
customPageRouter.use(apiRateLimiter)
customPageRouter.get('/homepage/instance',
asyncMiddleware(getInstanceHomepage)
)
customPageRouter.put('/homepage/instance',
authenticate,
ensureUserHasRight(UserRight.MANAGE_INSTANCE_CUSTOM_PAGE),
asyncMiddleware(updateInstanceHomepage)
)
// ---------------------------------------------------------------------------
export {
customPageRouter
}
// ---------------------------------------------------------------------------
async function getInstanceHomepage (req: express.Request, res: express.Response) {
const page = await ActorCustomPageModel.loadInstanceHomepage()
if (!page) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Instance homepage could not be found'
})
}
return res.json(page.toFormattedJSON())
}
async function updateInstanceHomepage (req: express.Request, res: express.Response) {
const content = req.body.content
await ActorCustomPageModel.updateInstanceHomepage(content)
ServerConfigManager.Instance.updateHomepageState(content)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
+76
ファイルの表示
@@ -0,0 +1,76 @@
import { HttpStatusCode } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import cors from 'cors'
import express from 'express'
import { abuseRouter } from './abuse.js'
import { accountsRouter } from './accounts.js'
import { automaticTagRouter } from './automatic-tags.js'
import { blocklistRouter } from './blocklist.js'
import { bulkRouter } from './bulk.js'
import { configRouter } from './config.js'
import { customPageRouter } from './custom-page.js'
import { jobsRouter } from './jobs.js'
import { metricsRouter } from './metrics.js'
import { oauthClientsRouter } from './oauth-clients.js'
import { overviewsRouter } from './overviews.js'
import { pluginRouter } from './plugins.js'
import { runnersRouter } from './runners/index.js'
import { searchRouter } from './search/index.js'
import { serverRouter } from './server/index.js'
import { usersRouter } from './users/index.js'
import { videoChannelSyncRouter } from './video-channel-sync.js'
import { videoChannelRouter } from './video-channel.js'
import { videoPlaylistRouter } from './video-playlist.js'
import { videosRouter } from './videos/index.js'
import { watchedWordsRouter } from './watched-words.js'
const apiRouter = express.Router()
apiRouter.use(cors({
origin: '*',
exposedHeaders: 'Retry-After',
credentials: true
}))
apiRouter.use('/server', serverRouter)
apiRouter.use('/abuses', abuseRouter)
apiRouter.use('/bulk', bulkRouter)
apiRouter.use('/oauth-clients', oauthClientsRouter)
apiRouter.use('/config', configRouter)
apiRouter.use('/users', usersRouter)
apiRouter.use('/accounts', accountsRouter)
apiRouter.use('/video-channels', videoChannelRouter)
apiRouter.use('/video-channel-syncs', videoChannelSyncRouter)
apiRouter.use('/video-playlists', videoPlaylistRouter)
apiRouter.use('/videos', videosRouter)
apiRouter.use('/jobs', jobsRouter)
apiRouter.use('/metrics', metricsRouter)
apiRouter.use('/search', searchRouter)
apiRouter.use('/overviews', overviewsRouter)
apiRouter.use('/plugins', pluginRouter)
apiRouter.use('/custom-pages', customPageRouter)
apiRouter.use('/blocklist', blocklistRouter)
apiRouter.use('/runners', runnersRouter)
apiRouter.use('/watched-words', watchedWordsRouter)
apiRouter.use('/automatic-tags', automaticTagRouter)
apiRouter.use('/ping', pong)
apiRouter.use('/*', badRequest)
// ---------------------------------------------------------------------------
export { apiRouter }
// ---------------------------------------------------------------------------
function pong (req: express.Request, res: express.Response) {
return res.send('pong').status(HttpStatusCode.OK_200).end()
}
function badRequest (req: express.Request, res: express.Response) {
logger.debug(`API express handler not found: bad PeerTube request for ${req.method} - ${req.originalUrl}`)
return res.type('json')
.status(HttpStatusCode.BAD_REQUEST_400)
.end()
}
+112
ファイルの表示
@@ -0,0 +1,112 @@
import { HttpStatusCode, Job, JobState, JobType, ResultList, UserRight } from '@peertube/peertube-models'
import { Job as BullJob } from 'bullmq'
import express from 'express'
import { isArray } from '../../helpers/custom-validators/misc.js'
import { JobQueue } from '../../lib/job-queue/index.js'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
ensureUserHasRight,
jobsSortValidator,
openapiOperationDoc,
paginationValidatorBuilder,
setDefaultPagination,
setDefaultSort
} from '../../middlewares/index.js'
import { listJobsValidator } from '../../middlewares/validators/jobs.js'
const jobsRouter = express.Router()
jobsRouter.use(apiRateLimiter)
jobsRouter.post('/pause',
authenticate,
ensureUserHasRight(UserRight.MANAGE_JOBS),
asyncMiddleware(pauseJobQueue)
)
jobsRouter.post('/resume',
authenticate,
ensureUserHasRight(UserRight.MANAGE_JOBS),
resumeJobQueue
)
jobsRouter.get('/:state?',
openapiOperationDoc({ operationId: 'getJobs' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_JOBS),
paginationValidatorBuilder([ 'jobs' ]),
jobsSortValidator,
setDefaultSort,
setDefaultPagination,
listJobsValidator,
asyncMiddleware(listJobs)
)
// ---------------------------------------------------------------------------
export {
jobsRouter
}
// ---------------------------------------------------------------------------
async function pauseJobQueue (req: express.Request, res: express.Response) {
await JobQueue.Instance.pause()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
function resumeJobQueue (req: express.Request, res: express.Response) {
JobQueue.Instance.resume()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function listJobs (req: express.Request, res: express.Response) {
const state = req.params.state as JobState
const asc = req.query.sort === 'createdAt'
const jobType = req.query.jobType
const jobs = await JobQueue.Instance.listForApi({
state,
start: req.query.start,
count: req.query.count,
asc,
jobType
})
const total = await JobQueue.Instance.count(state, jobType)
const result: ResultList<Job> = {
total,
data: await Promise.all(jobs.map(j => formatJob(j, state)))
}
return res.json(result)
}
async function formatJob (job: BullJob, state?: JobState): Promise<Job> {
return {
id: job.id,
state: state || await job.getState(),
type: job.queueName as JobType,
data: job.data,
parent: job.parent
? { id: job.parent.id }
: undefined,
progress: job.progress as number,
priority: job.opts.priority,
error: getJobError(job),
createdAt: new Date(job.timestamp),
finishedOn: new Date(job.finishedOn),
processedOn: new Date(job.processedOn)
}
}
function getJobError (job: BullJob) {
if (isArray(job.stacktrace) && job.stacktrace.length !== 0) return job.stacktrace[0]
if (job.failedReason) return job.failedReason
return null
}
+34
ファイルの表示
@@ -0,0 +1,34 @@
import express from 'express'
import { CONFIG } from '@server/initializers/config.js'
import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics.js'
import { HttpStatusCode, PlaybackMetricCreate } from '@peertube/peertube-models'
import { addPlaybackMetricValidator, apiRateLimiter, asyncMiddleware } from '../../middlewares/index.js'
const metricsRouter = express.Router()
metricsRouter.use(apiRateLimiter)
metricsRouter.post('/playback',
asyncMiddleware(addPlaybackMetricValidator),
addPlaybackMetric
)
// ---------------------------------------------------------------------------
export {
metricsRouter
}
// ---------------------------------------------------------------------------
function addPlaybackMetric (req: express.Request, res: express.Response) {
if (!CONFIG.OPEN_TELEMETRY.METRICS.ENABLED) {
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
}
const body: PlaybackMetricCreate = req.body
OpenTelemetryMetrics.Instance.observePlaybackMetric(res.locals.onlyImmutableVideo, body)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+54
ファイルの表示
@@ -0,0 +1,54 @@
import express from 'express'
import { HttpStatusCode, OAuthClientLocal } from '@peertube/peertube-models'
import { isTestOrDevInstance } from '@peertube/peertube-node-utils'
import { OAuthClientModel } from '@server/models/oauth/oauth-client.js'
import { logger } from '../../helpers/logger.js'
import { CONFIG } from '../../initializers/config.js'
import { apiRateLimiter, asyncMiddleware, openapiOperationDoc } from '../../middlewares/index.js'
const oauthClientsRouter = express.Router()
oauthClientsRouter.use(apiRateLimiter)
oauthClientsRouter.get('/local',
openapiOperationDoc({ operationId: 'getOAuthClient' }),
asyncMiddleware(getLocalClient)
)
// Get the client credentials for the PeerTube front end
async function getLocalClient (req: express.Request, res: express.Response, next: express.NextFunction) {
const serverHostname = CONFIG.WEBSERVER.HOSTNAME
const serverPort = CONFIG.WEBSERVER.PORT
let headerHostShouldBe = serverHostname
if (serverPort !== 80 && serverPort !== 443) {
headerHostShouldBe += ':' + serverPort
}
// Don't make this check if this is a test instance
if (!isTestOrDevInstance() && req.get('host') !== headerHostShouldBe) {
logger.info(
'Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe,
{ webserverConfig: CONFIG.WEBSERVER }
)
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: `Getting client tokens for host ${req.get('host')} is forbidden`
})
}
const client = await OAuthClientModel.loadFirstClient()
if (!client) throw new Error('No client available.')
const json: OAuthClientLocal = {
client_id: client.clientId,
client_secret: client.clientSecret
}
return res.json(json)
}
// ---------------------------------------------------------------------------
export {
oauthClientsRouter
}
+139
ファイルの表示
@@ -0,0 +1,139 @@
import express from 'express'
import memoizee from 'memoizee'
import { logger } from '@server/helpers/logger.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { getServerActor } from '@server/models/application/application.js'
import { VideoModel } from '@server/models/video/video.js'
import { CategoryOverview, ChannelOverview, TagOverview, VideosOverview } from '@peertube/peertube-models'
import { buildNSFWFilter } from '../../helpers/express-utils.js'
import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants.js'
import { apiRateLimiter, asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares/index.js'
import { TagModel } from '../../models/video/tag.js'
const overviewsRouter = express.Router()
overviewsRouter.use(apiRateLimiter)
overviewsRouter.get('/videos',
videosOverviewValidator,
optionalAuthenticate,
asyncMiddleware(getVideosOverview)
)
// ---------------------------------------------------------------------------
export { overviewsRouter }
// ---------------------------------------------------------------------------
const buildSamples = memoizee(async function () {
const [ categories, channels, tags ] = await Promise.all([
VideoModel.getRandomFieldSamples('category', OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT),
VideoModel.getRandomFieldSamples('channelId', OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT),
TagModel.getRandomSamples(OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT)
])
const result = { categories, channels, tags }
logger.debug('Building samples for overview endpoint.', { result })
return result
}, { maxAge: MEMOIZE_TTL.OVERVIEWS_SAMPLE })
// This endpoint could be quite long, but we cache it
async function getVideosOverview (req: express.Request, res: express.Response) {
const attributes = await buildSamples()
const page = req.query.page || 1
const index = page - 1
const categories: CategoryOverview[] = []
const channels: ChannelOverview[] = []
const tags: TagOverview[] = []
await Promise.all([
getVideosByCategory(attributes.categories, index, res, categories),
getVideosByChannel(attributes.channels, index, res, channels),
getVideosByTag(attributes.tags, index, res, tags)
])
const result: VideosOverview = {
categories,
channels,
tags
}
return res.json(result)
}
async function getVideosByTag (tagsSample: string[], index: number, res: express.Response, acc: TagOverview[]) {
if (tagsSample.length <= index) return
const tag = tagsSample[index]
const videos = await getVideos(res, { tagsOneOf: [ tag ] })
if (videos.length === 0) return
acc.push({
tag,
videos
})
}
async function getVideosByCategory (categoriesSample: number[], index: number, res: express.Response, acc: CategoryOverview[]) {
if (categoriesSample.length <= index) return
const category = categoriesSample[index]
const videos = await getVideos(res, { categoryOneOf: [ category ] })
if (videos.length === 0) return
acc.push({
category: videos[0].category,
videos
})
}
async function getVideosByChannel (channelsSample: number[], index: number, res: express.Response, acc: ChannelOverview[]) {
if (channelsSample.length <= index) return
const channelId = channelsSample[index]
const videos = await getVideos(res, { videoChannelId: channelId })
if (videos.length === 0) return
acc.push({
channel: videos[0].channel,
videos
})
}
async function getVideos (
res: express.Response,
where: { videoChannelId?: number, tagsOneOf?: string[], categoryOneOf?: number[] }
) {
const serverActor = await getServerActor()
const query = await Hooks.wrapObject({
start: 0,
count: 12,
sort: '-createdAt',
displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
},
nsfw: buildNSFWFilter(res),
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos: false,
...where
}, 'filter:api.overviews.videos.list.params')
const { data } = await Hooks.wrapPromiseFun(
VideoModel.listForApi.bind(VideoModel),
query,
'filter:api.overviews.videos.list.result'
)
return data.map(d => d.toFormattedJSON())
}
+230
ファイルの表示
@@ -0,0 +1,230 @@
import express from 'express'
import { logger } from '@server/helpers/logger.js'
import { getFormattedObjects } from '@server/helpers/utils.js'
import { listAvailablePluginsFromIndex } from '@server/lib/plugins/plugin-index.js'
import { PluginManager } from '@server/lib/plugins/plugin-manager.js'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
availablePluginsSortValidator,
ensureUserHasRight,
openapiOperationDoc,
paginationValidator,
pluginsSortValidator,
setDefaultPagination,
setDefaultSort
} from '@server/middlewares/index.js'
import {
existingPluginValidator,
installOrUpdatePluginValidator,
listAvailablePluginsValidator,
listPluginsValidator,
uninstallPluginValidator,
updatePluginSettingsValidator
} from '@server/middlewares/validators/plugins.js'
import { PluginModel } from '@server/models/server/plugin.js'
import {
HttpStatusCode,
InstallOrUpdatePlugin,
ManagePlugin,
PeertubePluginIndexList,
PublicServerSetting,
RegisteredServerSettings,
UserRight
} from '@peertube/peertube-models'
const pluginRouter = express.Router()
pluginRouter.use(apiRateLimiter)
pluginRouter.get('/available',
openapiOperationDoc({ operationId: 'getAvailablePlugins' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
listAvailablePluginsValidator,
paginationValidator,
availablePluginsSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listAvailablePlugins)
)
pluginRouter.get('/',
openapiOperationDoc({ operationId: 'getPlugins' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
listPluginsValidator,
paginationValidator,
pluginsSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listPlugins)
)
pluginRouter.get('/:npmName/registered-settings',
authenticate,
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
asyncMiddleware(existingPluginValidator),
getPluginRegisteredSettings
)
pluginRouter.get('/:npmName/public-settings',
asyncMiddleware(existingPluginValidator),
getPublicPluginSettings
)
pluginRouter.put('/:npmName/settings',
authenticate,
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
updatePluginSettingsValidator,
asyncMiddleware(existingPluginValidator),
asyncMiddleware(updatePluginSettings)
)
pluginRouter.get('/:npmName',
authenticate,
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
asyncMiddleware(existingPluginValidator),
getPlugin
)
pluginRouter.post('/install',
openapiOperationDoc({ operationId: 'addPlugin' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
installOrUpdatePluginValidator,
asyncMiddleware(installPlugin)
)
pluginRouter.post('/update',
openapiOperationDoc({ operationId: 'updatePlugin' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
installOrUpdatePluginValidator,
asyncMiddleware(updatePlugin)
)
pluginRouter.post('/uninstall',
openapiOperationDoc({ operationId: 'uninstallPlugin' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
uninstallPluginValidator,
asyncMiddleware(uninstallPlugin)
)
// ---------------------------------------------------------------------------
export {
pluginRouter
}
// ---------------------------------------------------------------------------
async function listPlugins (req: express.Request, res: express.Response) {
const pluginType = req.query.pluginType
const uninstalled = req.query.uninstalled
const resultList = await PluginModel.listForApi({
pluginType,
uninstalled,
start: req.query.start,
count: req.query.count,
sort: req.query.sort
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
function getPlugin (req: express.Request, res: express.Response) {
const plugin = res.locals.plugin
return res.json(plugin.toFormattedJSON())
}
async function installPlugin (req: express.Request, res: express.Response) {
const body: InstallOrUpdatePlugin = req.body
const fromDisk = !!body.path
const toInstall = body.npmName || body.path
const pluginVersion = body.pluginVersion && body.npmName
? body.pluginVersion
: undefined
try {
const plugin = await PluginManager.Instance.install({ toInstall, version: pluginVersion, fromDisk })
return res.json(plugin.toFormattedJSON())
} catch (err) {
logger.warn('Cannot install plugin %s.', toInstall, { err })
return res.fail({ message: 'Cannot install plugin ' + toInstall })
}
}
async function updatePlugin (req: express.Request, res: express.Response) {
const body: InstallOrUpdatePlugin = req.body
const fromDisk = !!body.path
const toUpdate = body.npmName || body.path
try {
const plugin = await PluginManager.Instance.update(toUpdate, fromDisk)
return res.json(plugin.toFormattedJSON())
} catch (err) {
logger.warn('Cannot update plugin %s.', toUpdate, { err })
return res.fail({ message: 'Cannot update plugin ' + toUpdate })
}
}
async function uninstallPlugin (req: express.Request, res: express.Response) {
const body: ManagePlugin = req.body
await PluginManager.Instance.uninstall({ npmName: body.npmName })
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
function getPublicPluginSettings (req: express.Request, res: express.Response) {
const plugin = res.locals.plugin
const registeredSettings = PluginManager.Instance.getRegisteredSettings(req.params.npmName)
const publicSettings = plugin.getPublicSettings(registeredSettings)
const json: PublicServerSetting = { publicSettings }
return res.json(json)
}
function getPluginRegisteredSettings (req: express.Request, res: express.Response) {
const registeredSettings = PluginManager.Instance.getRegisteredSettings(req.params.npmName)
const json: RegisteredServerSettings = { registeredSettings }
return res.json(json)
}
async function updatePluginSettings (req: express.Request, res: express.Response) {
const plugin = res.locals.plugin
plugin.settings = req.body.settings
await plugin.save()
await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function listAvailablePlugins (req: express.Request, res: express.Response) {
const query: PeertubePluginIndexList = req.query
const resultList = await listAvailablePluginsFromIndex(query)
if (!resultList) {
return res.fail({
status: HttpStatusCode.SERVICE_UNAVAILABLE_503,
message: 'Plugin index unavailable. Please retry later'
})
}
return res.json(resultList)
}
+20
ファイルの表示
@@ -0,0 +1,20 @@
import express from 'express'
import { runnerJobsRouter } from './jobs.js'
import { runnerJobFilesRouter } from './jobs-files.js'
import { manageRunnersRouter } from './manage-runners.js'
import { runnerRegistrationTokensRouter } from './registration-tokens.js'
const runnersRouter = express.Router()
// No api route limiter here, they are defined in child routers
runnersRouter.use('/', manageRunnersRouter)
runnersRouter.use('/', runnerJobsRouter)
runnersRouter.use('/', runnerJobFilesRouter)
runnersRouter.use('/', runnerRegistrationTokensRouter)
// ---------------------------------------------------------------------------
export {
runnersRouter
}
+112
ファイルの表示
@@ -0,0 +1,112 @@
import express from 'express'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { proxifyHLS, proxifyWebVideoFile } from '@server/lib/object-storage/index.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { getStudioTaskFilePath } from '@server/lib/video-studio.js'
import { apiRateLimiter, asyncMiddleware } from '@server/middlewares/index.js'
import { jobOfRunnerGetValidatorFactory } from '@server/middlewares/validators/runners/index.js'
import {
runnerJobGetVideoStudioTaskFileValidator,
runnerJobGetVideoTranscodingFileValidator
} from '@server/middlewares/validators/runners/job-files.js'
import { RunnerJobState, FileStorage } from '@peertube/peertube-models'
const lTags = loggerTagsFactory('api', 'runner')
const runnerJobFilesRouter = express.Router()
runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/max-quality',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])),
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
asyncMiddleware(getMaxQualityVideoFile)
)
runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/previews/max-quality',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])),
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
getMaxQualityVideoPreview
)
runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/studio/task-files/:filename',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])),
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
runnerJobGetVideoStudioTaskFileValidator,
getVideoStudioTaskFile
)
// ---------------------------------------------------------------------------
export {
runnerJobFilesRouter
}
// ---------------------------------------------------------------------------
async function getMaxQualityVideoFile (req: express.Request, res: express.Response) {
const runnerJob = res.locals.runnerJob
const runner = runnerJob.Runner
const video = res.locals.videoAll
logger.info(
'Get max quality file of video %s of job %s for runner %s', video.uuid, runnerJob.uuid, runner.name,
lTags(runner.name, runnerJob.id, runnerJob.type)
)
const file = video.getMaxQualityFile()
if (file.storage === FileStorage.OBJECT_STORAGE) {
if (file.isHLS()) {
return proxifyHLS({
req,
res,
filename: file.filename,
playlist: video.getHLSPlaylist(),
reinjectVideoFileToken: false,
video
})
}
// Web video
return proxifyWebVideoFile({
req,
res,
filename: file.filename
})
}
return VideoPathManager.Instance.makeAvailableVideoFile(file, videoPath => {
return res.sendFile(videoPath)
})
}
function getMaxQualityVideoPreview (req: express.Request, res: express.Response) {
const runnerJob = res.locals.runnerJob
const runner = runnerJob.Runner
const video = res.locals.videoAll
logger.info(
'Get max quality preview file of video %s of job %s for runner %s', video.uuid, runnerJob.uuid, runner.name,
lTags(runner.name, runnerJob.id, runnerJob.type)
)
const file = video.getPreview()
return res.sendFile(file.getPath())
}
function getVideoStudioTaskFile (req: express.Request, res: express.Response) {
const runnerJob = res.locals.runnerJob
const runner = runnerJob.Runner
const video = res.locals.videoAll
const filename = req.params.filename
logger.info(
'Get video studio task file %s of video %s of job %s for runner %s', filename, video.uuid, runnerJob.uuid, runner.name,
lTags(runner.name, runnerJob.id, runnerJob.type)
)
return res.sendFile(getStudioTaskFilePath(filename))
}
+425
ファイルの表示
@@ -0,0 +1,425 @@
import {
AbortRunnerJobBody,
AcceptRunnerJobResult,
ErrorRunnerJobBody,
HttpStatusCode,
ListRunnerJobsQuery,
LiveRTMPHLSTranscodingUpdatePayload,
RequestRunnerJobResult,
RunnerJobState,
RunnerJobSuccessBody,
RunnerJobSuccessPayload,
RunnerJobType,
RunnerJobUpdateBody,
RunnerJobUpdatePayload,
ServerErrorCode,
TranscriptionSuccess,
UserRight,
VODAudioMergeTranscodingSuccess,
VODHLSTranscodingSuccess,
VODWebVideoTranscodingSuccess,
VideoStudioTranscodingSuccess
} from '@peertube/peertube-models'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { createReqFiles } from '@server/helpers/express-utils.js'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { generateRunnerJobToken } from '@server/helpers/token-generator.js'
import { MIMETYPES } from '@server/initializers/constants.js'
import { sequelizeTypescript } from '@server/initializers/database.js'
import { getRunnerJobHandlerClass, runnerJobCanBeCancelled, updateLastRunnerContact } from '@server/lib/runners/index.js'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
ensureUserHasRight,
paginationValidator,
runnerJobsSortValidator,
setDefaultPagination,
setDefaultSort
} from '@server/middlewares/index.js'
import {
abortRunnerJobValidator,
acceptRunnerJobValidator,
cancelRunnerJobValidator,
errorRunnerJobValidator,
getRunnerFromTokenValidator,
jobOfRunnerGetValidatorFactory,
listRunnerJobsValidator,
runnerJobGetValidator,
successRunnerJobValidator,
updateRunnerJobValidator
} from '@server/middlewares/validators/runners/index.js'
import { RunnerJobModel } from '@server/models/runner/runner-job.js'
import { RunnerModel } from '@server/models/runner/runner.js'
import express, { UploadFiles } from 'express'
const postRunnerJobSuccessVideoFiles = createReqFiles(
[ 'payload[videoFile]', 'payload[resolutionPlaylistFile]', 'payload[vttFile]' ],
{ ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.M3U8.MIMETYPE_EXT, ...MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT }
)
const runnerJobUpdateVideoFiles = createReqFiles(
[ 'payload[videoChunkFile]', 'payload[resolutionPlaylistFile]', 'payload[masterPlaylistFile]' ],
{ ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.M3U8.MIMETYPE_EXT }
)
const lTags = loggerTagsFactory('api', 'runner')
const runnerJobsRouter = express.Router()
// ---------------------------------------------------------------------------
// Controllers for runners
// ---------------------------------------------------------------------------
runnerJobsRouter.post('/jobs/request',
apiRateLimiter,
asyncMiddleware(getRunnerFromTokenValidator),
asyncMiddleware(requestRunnerJob)
)
runnerJobsRouter.post('/jobs/:jobUUID/accept',
apiRateLimiter,
asyncMiddleware(runnerJobGetValidator),
acceptRunnerJobValidator,
asyncMiddleware(getRunnerFromTokenValidator),
asyncMiddleware(acceptRunnerJob)
)
runnerJobsRouter.post('/jobs/:jobUUID/abort',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])),
abortRunnerJobValidator,
asyncMiddleware(abortRunnerJob)
)
runnerJobsRouter.post('/jobs/:jobUUID/update',
runnerJobUpdateVideoFiles,
apiRateLimiter, // Has to be after multer middleware to parse runner token
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING, RunnerJobState.COMPLETING, RunnerJobState.COMPLETED ])),
updateRunnerJobValidator,
asyncMiddleware(updateRunnerJobController)
)
runnerJobsRouter.post('/jobs/:jobUUID/error',
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])),
errorRunnerJobValidator,
asyncMiddleware(errorRunnerJob)
)
runnerJobsRouter.post('/jobs/:jobUUID/success',
postRunnerJobSuccessVideoFiles,
apiRateLimiter, // Has to be after multer middleware to parse runner token
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])),
successRunnerJobValidator,
asyncMiddleware(postRunnerJobSuccess)
)
// ---------------------------------------------------------------------------
// Controllers for admins
// ---------------------------------------------------------------------------
runnerJobsRouter.post('/jobs/:jobUUID/cancel',
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(runnerJobGetValidator),
cancelRunnerJobValidator,
asyncMiddleware(cancelRunnerJob)
)
runnerJobsRouter.get('/jobs',
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
paginationValidator,
runnerJobsSortValidator,
setDefaultSort,
setDefaultPagination,
listRunnerJobsValidator,
asyncMiddleware(listRunnerJobs)
)
runnerJobsRouter.delete('/jobs/:jobUUID',
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(runnerJobGetValidator),
asyncMiddleware(deleteRunnerJob)
)
// ---------------------------------------------------------------------------
export {
runnerJobsRouter
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Controllers for runners
// ---------------------------------------------------------------------------
async function requestRunnerJob (req: express.Request, res: express.Response) {
const runner = res.locals.runner
const availableJobs = await RunnerJobModel.listAvailableJobs()
logger.debug('Runner %s requests for a job.', runner.name, { availableJobs, ...lTags(runner.name) })
const result: RequestRunnerJobResult = {
availableJobs: availableJobs.map(j => ({
uuid: j.uuid,
type: j.type,
payload: j.payload
}))
}
updateLastRunnerContact(req, runner)
return res.json(result)
}
async function acceptRunnerJob (req: express.Request, res: express.Response) {
const runner = res.locals.runner
const runnerJob = res.locals.runnerJob
const newRunnerJob = await retryTransactionWrapper(() => {
return sequelizeTypescript.transaction(async transaction => {
await runnerJob.reload({ transaction })
if (runnerJob.state !== RunnerJobState.PENDING) {
res.fail({
type: ServerErrorCode.RUNNER_JOB_NOT_IN_PENDING_STATE,
message: 'This job is not in pending state anymore',
status: HttpStatusCode.CONFLICT_409
})
return undefined
}
runnerJob.state = RunnerJobState.PROCESSING
runnerJob.processingJobToken = generateRunnerJobToken()
runnerJob.startedAt = new Date()
runnerJob.runnerId = runner.id
return runnerJob.save({ transaction })
})
})
if (!newRunnerJob) return
newRunnerJob.Runner = runner as RunnerModel
const result: AcceptRunnerJobResult = {
job: {
...newRunnerJob.toFormattedJSON(),
jobToken: newRunnerJob.processingJobToken
}
}
updateLastRunnerContact(req, runner)
logger.info(
'Remote runner %s has accepted job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type,
lTags(runner.name, runnerJob.uuid, runnerJob.type)
)
return res.json(result)
}
async function abortRunnerJob (req: express.Request, res: express.Response) {
const runnerJob = res.locals.runnerJob
const runner = runnerJob.Runner
const body: AbortRunnerJobBody = req.body
logger.info(
'Remote runner %s is aborting job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type,
{ reason: body.reason, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
)
const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
await new RunnerJobHandler().abort({ runnerJob })
updateLastRunnerContact(req, runnerJob.Runner)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function errorRunnerJob (req: express.Request, res: express.Response) {
const runnerJob = res.locals.runnerJob
const runner = runnerJob.Runner
const body: ErrorRunnerJobBody = req.body
runnerJob.failures += 1
logger.error(
'Remote runner %s had an error with job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type,
{ errorMessage: body.message, totalFailures: runnerJob.failures, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
)
const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
await new RunnerJobHandler().error({ runnerJob, message: body.message })
updateLastRunnerContact(req, runnerJob.Runner)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
// ---------------------------------------------------------------------------
const jobUpdateBuilders: {
[id in RunnerJobType]?: (payload: RunnerJobUpdatePayload, files?: UploadFiles) => RunnerJobUpdatePayload
} = {
'live-rtmp-hls-transcoding': (payload: LiveRTMPHLSTranscodingUpdatePayload, files) => {
return {
...payload,
masterPlaylistFile: files['payload[masterPlaylistFile]']?.[0].path,
resolutionPlaylistFile: files['payload[resolutionPlaylistFile]']?.[0].path,
videoChunkFile: files['payload[videoChunkFile]']?.[0].path
}
}
}
async function updateRunnerJobController (req: express.Request, res: express.Response) {
const runnerJob = res.locals.runnerJob
const runner = runnerJob.Runner
const body: RunnerJobUpdateBody = req.body
if (runnerJob.state === RunnerJobState.COMPLETING || runnerJob.state === RunnerJobState.COMPLETED) {
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
const payloadBuilder = jobUpdateBuilders[runnerJob.type]
const updatePayload = payloadBuilder
? payloadBuilder(body.payload, req.files as UploadFiles)
: undefined
logger.debug(
'Remote runner %s is updating job %s (%s)', runnerJob.Runner.name, runnerJob.uuid, runnerJob.type,
{ body, updatePayload, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
)
const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
await new RunnerJobHandler().update({
runnerJob,
progress: req.body.progress,
updatePayload
})
updateLastRunnerContact(req, runnerJob.Runner)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
// ---------------------------------------------------------------------------
const jobSuccessPayloadBuilders: {
[id in RunnerJobType]: (payload: RunnerJobSuccessPayload, files?: UploadFiles) => RunnerJobSuccessPayload
} = {
'vod-web-video-transcoding': (payload: VODWebVideoTranscodingSuccess, files) => {
return {
...payload,
videoFile: files['payload[videoFile]'][0].path
}
},
'vod-hls-transcoding': (payload: VODHLSTranscodingSuccess, files) => {
return {
...payload,
videoFile: files['payload[videoFile]'][0].path,
resolutionPlaylistFile: files['payload[resolutionPlaylistFile]'][0].path
}
},
'vod-audio-merge-transcoding': (payload: VODAudioMergeTranscodingSuccess, files) => {
return {
...payload,
videoFile: files['payload[videoFile]'][0].path
}
},
'video-studio-transcoding': (payload: VideoStudioTranscodingSuccess, files) => {
return {
...payload,
videoFile: files['payload[videoFile]'][0].path
}
},
'live-rtmp-hls-transcoding': () => ({}),
'video-transcription': (payload: TranscriptionSuccess, files) => {
return {
...payload,
vttFile: files['payload[vttFile]'][0].path
}
}
}
async function postRunnerJobSuccess (req: express.Request, res: express.Response) {
const runnerJob = res.locals.runnerJob
const runner = runnerJob.Runner
const body: RunnerJobSuccessBody = req.body
const resultPayload = jobSuccessPayloadBuilders[runnerJob.type](body.payload, req.files as UploadFiles)
logger.info(
'Remote runner %s is sending success result for job %s (%s)', runnerJob.Runner.name, runnerJob.uuid, runnerJob.type,
{ resultPayload, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
)
const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
await new RunnerJobHandler().complete({ runnerJob, resultPayload })
updateLastRunnerContact(req, runnerJob.Runner)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
// ---------------------------------------------------------------------------
// Controllers for admins
// ---------------------------------------------------------------------------
async function cancelRunnerJob (req: express.Request, res: express.Response) {
const runnerJob = res.locals.runnerJob
logger.info('Cancelling job %s (%s)', runnerJob.uuid, runnerJob.type, lTags(runnerJob.uuid, runnerJob.type))
const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
await new RunnerJobHandler().cancel({ runnerJob })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function deleteRunnerJob (req: express.Request, res: express.Response) {
const runnerJob = res.locals.runnerJob
logger.info('Deleting job %s (%s)', runnerJob.uuid, runnerJob.type, lTags(runnerJob.uuid, runnerJob.type))
if (runnerJobCanBeCancelled(runnerJob)) {
const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
await new RunnerJobHandler().cancel({ runnerJob })
}
await runnerJob.destroy()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function listRunnerJobs (req: express.Request, res: express.Response) {
const query: ListRunnerJobsQuery = req.query
const resultList = await RunnerJobModel.listForApi({
start: query.start,
count: query.count,
sort: query.sort,
search: query.search,
stateOneOf: query.stateOneOf
})
return res.json({
total: resultList.total,
data: resultList.data.map(d => d.toFormattedAdminJSON())
})
}
+116
ファイルの表示
@@ -0,0 +1,116 @@
import express from 'express'
import { HttpStatusCode, ListRunnersQuery, RegisterRunnerBody, UserRight } from '@peertube/peertube-models'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { generateRunnerToken } from '@server/helpers/token-generator.js'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
ensureUserHasRight,
paginationValidator,
runnersSortValidator,
setDefaultPagination,
setDefaultSort
} from '@server/middlewares/index.js'
import {
deleteRunnerValidator,
getRunnerFromTokenValidator,
registerRunnerValidator
} from '@server/middlewares/validators/runners/index.js'
import { RunnerModel } from '@server/models/runner/runner.js'
const lTags = loggerTagsFactory('api', 'runner')
const manageRunnersRouter = express.Router()
manageRunnersRouter.post('/register',
apiRateLimiter,
asyncMiddleware(registerRunnerValidator),
asyncMiddleware(registerRunner)
)
manageRunnersRouter.post('/unregister',
apiRateLimiter,
asyncMiddleware(getRunnerFromTokenValidator),
asyncMiddleware(unregisterRunner)
)
manageRunnersRouter.delete('/:runnerId',
apiRateLimiter,
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(deleteRunnerValidator),
asyncMiddleware(deleteRunner)
)
manageRunnersRouter.get('/',
apiRateLimiter,
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
paginationValidator,
runnersSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listRunners)
)
// ---------------------------------------------------------------------------
export {
manageRunnersRouter
}
// ---------------------------------------------------------------------------
async function registerRunner (req: express.Request, res: express.Response) {
const body: RegisterRunnerBody = req.body
const runnerToken = generateRunnerToken()
const runner = new RunnerModel({
runnerToken,
name: body.name,
description: body.description,
lastContact: new Date(),
ip: req.ip,
runnerRegistrationTokenId: res.locals.runnerRegistrationToken.id
})
await runner.save()
logger.info('Registered new runner %s', runner.name, { ...lTags(runner.name) })
return res.json({ id: runner.id, runnerToken })
}
async function unregisterRunner (req: express.Request, res: express.Response) {
const runner = res.locals.runner
await runner.destroy()
logger.info('Unregistered runner %s', runner.name, { ...lTags(runner.name) })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function deleteRunner (req: express.Request, res: express.Response) {
const runner = res.locals.runner
await runner.destroy()
logger.info('Deleted runner %s', runner.name, { ...lTags(runner.name) })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function listRunners (req: express.Request, res: express.Response) {
const query: ListRunnersQuery = req.query
const resultList = await RunnerModel.listForApi({
start: query.start,
count: query.count,
sort: query.sort
})
return res.json({
total: resultList.total,
data: resultList.data.map(d => d.toFormattedJSON())
})
}
+91
ファイルの表示
@@ -0,0 +1,91 @@
import express from 'express'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { generateRunnerRegistrationToken } from '@server/helpers/token-generator.js'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
ensureUserHasRight,
paginationValidator,
runnerRegistrationTokensSortValidator,
setDefaultPagination,
setDefaultSort
} from '@server/middlewares/index.js'
import { deleteRegistrationTokenValidator } from '@server/middlewares/validators/runners/index.js'
import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token.js'
import { HttpStatusCode, ListRunnerRegistrationTokensQuery, UserRight } from '@peertube/peertube-models'
const lTags = loggerTagsFactory('api', 'runner')
const runnerRegistrationTokensRouter = express.Router()
runnerRegistrationTokensRouter.post('/registration-tokens/generate',
apiRateLimiter,
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(generateRegistrationToken)
)
runnerRegistrationTokensRouter.delete('/registration-tokens/:id',
apiRateLimiter,
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(deleteRegistrationTokenValidator),
asyncMiddleware(deleteRegistrationToken)
)
runnerRegistrationTokensRouter.get('/registration-tokens',
apiRateLimiter,
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
paginationValidator,
runnerRegistrationTokensSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listRegistrationTokens)
)
// ---------------------------------------------------------------------------
export {
runnerRegistrationTokensRouter
}
// ---------------------------------------------------------------------------
async function generateRegistrationToken (req: express.Request, res: express.Response) {
logger.info('Generating new runner registration token.', lTags())
const registrationToken = new RunnerRegistrationTokenModel({
registrationToken: generateRunnerRegistrationToken()
})
await registrationToken.save()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function deleteRegistrationToken (req: express.Request, res: express.Response) {
logger.info('Removing runner registration token.', lTags())
const runnerRegistrationToken = res.locals.runnerRegistrationToken
await runnerRegistrationToken.destroy()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function listRegistrationTokens (req: express.Request, res: express.Response) {
const query: ListRunnerRegistrationTokensQuery = req.query
const resultList = await RunnerRegistrationTokenModel.listForApi({
start: query.start,
count: query.count,
sort: query.sort
})
return res.json({
total: resultList.total,
data: resultList.data.map(d => d.toFormattedJSON())
})
}
+19
ファイルの表示
@@ -0,0 +1,19 @@
import express from 'express'
import { apiRateLimiter } from '@server/middlewares/index.js'
import { searchChannelsRouter } from './search-video-channels.js'
import { searchPlaylistsRouter } from './search-video-playlists.js'
import { searchVideosRouter } from './search-videos.js'
const searchRouter = express.Router()
searchRouter.use(apiRateLimiter)
searchRouter.use('/', searchVideosRouter)
searchRouter.use('/', searchChannelsRouter)
searchRouter.use('/', searchPlaylistsRouter)
// ---------------------------------------------------------------------------
export {
searchRouter
}
+151
ファイルの表示
@@ -0,0 +1,151 @@
import express from 'express'
import { sanitizeUrl } from '@server/helpers/core-utils.js'
import { pickSearchChannelQuery } from '@server/helpers/query.js'
import { doJSONRequest } from '@server/helpers/requests.js'
import { CONFIG } from '@server/initializers/config.js'
import { WEBSERVER } from '@server/initializers/constants.js'
import { findLatestAPRedirection } from '@server/lib/activitypub/activity.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search.js'
import { getServerActor } from '@server/models/application/application.js'
import { HttpStatusCode, ResultList, VideoChannel, VideoChannelsSearchQueryAfterSanitize } from '@peertube/peertube-models'
import { isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils.js'
import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../../../lib/activitypub/actors/index.js'
import {
asyncMiddleware,
openapiOperationDoc,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
setDefaultSearchSort,
videoChannelsListSearchValidator,
videoChannelsSearchSortValidator
} from '../../../middlewares/index.js'
import { VideoChannelModel } from '../../../models/video/video-channel.js'
import { MChannelAccountDefault } from '../../../types/models/index.js'
import { searchLocalUrl } from './shared/index.js'
const searchChannelsRouter = express.Router()
searchChannelsRouter.get('/video-channels',
openapiOperationDoc({ operationId: 'searchChannels' }),
paginationValidator,
setDefaultPagination,
videoChannelsSearchSortValidator,
setDefaultSearchSort,
optionalAuthenticate,
videoChannelsListSearchValidator,
asyncMiddleware(searchVideoChannels)
)
// ---------------------------------------------------------------------------
export { searchChannelsRouter }
// ---------------------------------------------------------------------------
function searchVideoChannels (req: express.Request, res: express.Response) {
const query = pickSearchChannelQuery(req.query)
const search = query.search || ''
const parts = search.split('@')
// Handle strings like @toto@example.com
if (parts.length === 3 && parts[0].length === 0) parts.shift()
const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' '))
if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, res)
// @username -> username to search in DB
if (search.startsWith('@')) query.search = search.replace(/^@/, '')
if (isSearchIndexSearch(query)) {
return searchVideoChannelsIndex(query, res)
}
return searchVideoChannelsDB(query, res)
}
async function searchVideoChannelsIndex (query: VideoChannelsSearchQueryAfterSanitize, res: express.Response) {
const result = await buildMutedForSearchIndex(res)
const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params')
const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels'
try {
logger.debug('Doing video channels search index request on %s.', url, { body })
const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body })
const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result')
return res.json(jsonResult)
} catch (err) {
logger.warn('Cannot use search index to make video channels search.', { err })
return res.fail({
status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
message: 'Cannot use search index to make video channels search'
})
}
}
async function searchVideoChannelsDB (query: VideoChannelsSearchQueryAfterSanitize, res: express.Response) {
const serverActor = await getServerActor()
const apiOptions = await Hooks.wrapObject({
...query,
actorId: serverActor.id
}, 'filter:api.search.video-channels.local.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoChannelModel.searchForApi.bind(VideoChannelModel),
apiOptions,
'filter:api.search.video-channels.local.list.result'
)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function searchVideoChannelURI (search: string, res: express.Response) {
let videoChannel: MChannelAccountDefault
let uri = search
if (!isURISearch(search)) {
try {
uri = await loadActorUrlOrGetFromWebfinger(search)
} catch (err) {
logger.warn('Cannot load actor URL or get from webfinger.', { search, err })
return res.json({ total: 0, data: [] })
}
}
if (isUserAbleToSearchRemoteURI(res)) {
try {
const latestUri = await findLatestAPRedirection(uri)
const actor = await getOrCreateAPActor(latestUri, 'all', true, true)
videoChannel = actor.VideoChannel
} catch (err) {
logger.info('Cannot search remote video channel %s.', uri, { err })
}
} else {
videoChannel = await searchLocalUrl(sanitizeLocalUrl(uri), url => VideoChannelModel.loadByUrlAndPopulateAccount(url))
}
return res.json({
total: videoChannel ? 1 : 0,
data: videoChannel ? [ videoChannel.toFormattedJSON() ] : []
})
}
function sanitizeLocalUrl (url: string) {
if (!url) return ''
// Handle alternative channel URLs
return url.replace(new RegExp('^' + WEBSERVER.URL + '/c/'), WEBSERVER.URL + '/video-channels/')
}
+131
ファイルの表示
@@ -0,0 +1,131 @@
import express from 'express'
import { sanitizeUrl } from '@server/helpers/core-utils.js'
import { isUserAbleToSearchRemoteURI } from '@server/helpers/express-utils.js'
import { logger } from '@server/helpers/logger.js'
import { pickSearchPlaylistQuery } from '@server/helpers/query.js'
import { doJSONRequest } from '@server/helpers/requests.js'
import { getFormattedObjects } from '@server/helpers/utils.js'
import { CONFIG } from '@server/initializers/config.js'
import { WEBSERVER } from '@server/initializers/constants.js'
import { findLatestAPRedirection } from '@server/lib/activitypub/activity.js'
import { getOrCreateAPVideoPlaylist } from '@server/lib/activitypub/playlists/get.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search.js'
import { getServerActor } from '@server/models/application/application.js'
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
import { MVideoPlaylistFullSummary } from '@server/types/models/index.js'
import { HttpStatusCode, ResultList, VideoPlaylist, VideoPlaylistsSearchQueryAfterSanitize } from '@peertube/peertube-models'
import {
asyncMiddleware,
openapiOperationDoc,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
setDefaultSearchSort,
videoPlaylistsListSearchValidator,
videoPlaylistsSearchSortValidator
} from '../../../middlewares/index.js'
import { searchLocalUrl } from './shared/index.js'
const searchPlaylistsRouter = express.Router()
searchPlaylistsRouter.get('/video-playlists',
openapiOperationDoc({ operationId: 'searchPlaylists' }),
paginationValidator,
setDefaultPagination,
videoPlaylistsSearchSortValidator,
setDefaultSearchSort,
optionalAuthenticate,
videoPlaylistsListSearchValidator,
asyncMiddleware(searchVideoPlaylists)
)
// ---------------------------------------------------------------------------
export { searchPlaylistsRouter }
// ---------------------------------------------------------------------------
function searchVideoPlaylists (req: express.Request, res: express.Response) {
const query = pickSearchPlaylistQuery(req.query)
const search = query.search
if (isURISearch(search)) return searchVideoPlaylistsURI(search, res)
if (isSearchIndexSearch(query)) {
return searchVideoPlaylistsIndex(query, res)
}
return searchVideoPlaylistsDB(query, res)
}
async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQueryAfterSanitize, res: express.Response) {
const result = await buildMutedForSearchIndex(res)
const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-playlists.index.list.params')
const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-playlists'
try {
logger.debug('Doing video playlists search index request on %s.', url, { body })
const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoPlaylist>>(url, { method: 'POST', json: body })
const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-playlists.index.list.result')
return res.json(jsonResult)
} catch (err) {
logger.warn('Cannot use search index to make video playlists search.', { err })
return res.fail({
status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
message: 'Cannot use search index to make video playlists search'
})
}
}
async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQueryAfterSanitize, res: express.Response) {
const serverActor = await getServerActor()
const apiOptions = await Hooks.wrapObject({
...query,
followerActorId: serverActor.id
}, 'filter:api.search.video-playlists.local.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoPlaylistModel.searchForApi.bind(VideoPlaylistModel),
apiOptions,
'filter:api.search.video-playlists.local.list.result'
)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function searchVideoPlaylistsURI (search: string, res: express.Response) {
let videoPlaylist: MVideoPlaylistFullSummary
if (isUserAbleToSearchRemoteURI(res)) {
try {
const url = await findLatestAPRedirection(search)
videoPlaylist = await getOrCreateAPVideoPlaylist(url)
} catch (err) {
logger.info('Cannot search remote video playlist %s.', search, { err })
}
} else {
videoPlaylist = await searchLocalUrl(sanitizeLocalUrl(search), url => VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(url))
}
return res.json({
total: videoPlaylist ? 1 : 0,
data: videoPlaylist ? [ videoPlaylist.toFormattedJSON() ] : []
})
}
function sanitizeLocalUrl (url: string) {
if (!url) return ''
// Handle alternative channel URLs
return url.replace(new RegExp('^' + WEBSERVER.URL + '/videos/watch/playlist/'), WEBSERVER.URL + '/video-playlists/')
.replace(new RegExp('^' + WEBSERVER.URL + '/w/p/'), WEBSERVER.URL + '/video-playlists/')
}
+168
ファイルの表示
@@ -0,0 +1,168 @@
import express from 'express'
import { sanitizeUrl } from '@server/helpers/core-utils.js'
import { pickSearchVideoQuery } from '@server/helpers/query.js'
import { doJSONRequest } from '@server/helpers/requests.js'
import { CONFIG } from '@server/initializers/config.js'
import { WEBSERVER } from '@server/initializers/constants.js'
import { findLatestAPRedirection } from '@server/lib/activitypub/activity.js'
import { getOrCreateAPVideo } from '@server/lib/activitypub/videos/index.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search.js'
import { getServerActor } from '@server/models/application/application.js'
import { HttpStatusCode, ResultList, Video, VideosSearchQueryAfterSanitize } from '@peertube/peertube-models'
import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils.js'
import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import {
asyncMiddleware,
commonVideosFiltersValidator,
openapiOperationDoc,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
setDefaultSearchSort,
videosSearchSortValidator,
videosSearchValidator
} from '../../../middlewares/index.js'
import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter/index.js'
import { VideoModel } from '../../../models/video/video.js'
import { MVideoAccountLightBlacklistAllFiles } from '../../../types/models/index.js'
import { searchLocalUrl } from './shared/index.js'
const searchVideosRouter = express.Router()
searchVideosRouter.get('/videos',
openapiOperationDoc({ operationId: 'searchVideos' }),
paginationValidator,
setDefaultPagination,
videosSearchSortValidator,
setDefaultSearchSort,
optionalAuthenticate,
commonVideosFiltersValidator,
videosSearchValidator,
asyncMiddleware(searchVideos)
)
// ---------------------------------------------------------------------------
export { searchVideosRouter }
// ---------------------------------------------------------------------------
function searchVideos (req: express.Request, res: express.Response) {
const query = pickSearchVideoQuery(req.query)
const search = query.search
if (isURISearch(search)) {
return searchVideoURI(search, res)
}
if (isSearchIndexSearch(query)) {
return searchVideosIndex(query, res)
}
return searchVideosDB(query, req, res)
}
async function searchVideosIndex (query: VideosSearchQueryAfterSanitize, res: express.Response) {
const result = await buildMutedForSearchIndex(res)
let body = { ...query, ...result }
// Use the default instance NSFW policy if not specified
if (!body.nsfw) {
const nsfwPolicy = res.locals.oauth
? res.locals.oauth.token.User.nsfwPolicy
: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
body.nsfw = nsfwPolicy === 'do_not_list'
? 'false'
: 'both'
}
body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params')
const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos'
try {
logger.debug('Doing videos search index request on %s.', url, { body })
const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body })
const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result')
return res.json(jsonResult)
} catch (err) {
logger.warn('Cannot use search index to make video search.', { err })
return res.fail({
status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
message: 'Cannot use search index to make video search'
})
}
}
async function searchVideosDB (query: VideosSearchQueryAfterSanitize, req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const apiOptions = await Hooks.wrapObject({
...query,
displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
},
countVideos: getCountVideos(req),
nsfw: buildNSFWFilter(res, query.nsfw),
user: res.locals.oauth
? res.locals.oauth.token.User
: undefined
}, 'filter:api.search.videos.local.list.params', { req, res })
const resultList = await Hooks.wrapPromiseFun(
VideoModel.searchAndPopulateAccountAndServer.bind(VideoModel),
apiOptions,
'filter:api.search.videos.local.list.result'
)
return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
}
async function searchVideoURI (url: string, res: express.Response) {
let video: MVideoAccountLightBlacklistAllFiles
// Check if we can fetch a remote video with the URL
if (isUserAbleToSearchRemoteURI(res)) {
try {
const syncParam = {
rates: false,
shares: false,
comments: false,
refreshVideo: false
}
const result = await getOrCreateAPVideo({
videoObject: await findLatestAPRedirection(url),
syncParam
})
video = result ? result.video : undefined
} catch (err) {
logger.info('Cannot search remote video %s.', url, { err })
}
} else {
video = await searchLocalUrl(sanitizeLocalUrl(url), url => VideoModel.loadByUrlAndPopulateAccountAndFiles(url))
}
return res.json({
total: video ? 1 : 0,
data: video ? [ video.toFormattedJSON() ] : []
})
}
function sanitizeLocalUrl (url: string) {
if (!url) return ''
// Handle alternative video URLs
return url.replace(new RegExp('^' + WEBSERVER.URL + '/w/'), WEBSERVER.URL + '/videos/watch/')
}
+1
ファイルの表示
@@ -0,0 +1 @@
export * from './utils.js'
+16
ファイルの表示
@@ -0,0 +1,16 @@
async function searchLocalUrl <T> (url: string, finder: (url: string) => Promise<T>) {
const data = await finder(url)
if (data) return data
return finder(removeQueryParams(url))
}
export {
searchLocalUrl
}
// ---------------------------------------------------------------------------
function removeQueryParams (url: string) {
return url.split('?').shift()
}
+33
ファイルの表示
@@ -0,0 +1,33 @@
import express from 'express'
import { ContactForm, HttpStatusCode } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { Emailer } from '../../../lib/emailer.js'
import { Redis } from '../../../lib/redis.js'
import { asyncMiddleware, contactAdministratorValidator } from '../../../middlewares/index.js'
const contactRouter = express.Router()
contactRouter.post('/contact',
asyncMiddleware(contactAdministratorValidator),
asyncMiddleware(contactAdministrator)
)
async function contactAdministrator (req: express.Request, res: express.Response) {
const data = req.body as ContactForm
Emailer.Instance.addContactFormJob(data.fromEmail, data.fromName, data.subject, data.body)
try {
await Redis.Instance.setContactFormIp(req.ip)
} catch (err) {
logger.error(err)
}
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
// ---------------------------------------------------------------------------
export {
contactRouter
}
+60
ファイルの表示
@@ -0,0 +1,60 @@
import express from 'express'
import { Debug, HttpStatusCode, SendDebugCommand, UserRight } from '@peertube/peertube-models'
import { InboxManager } from '@server/lib/activitypub/inbox-manager.js'
import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.js'
import { UpdateVideosScheduler } from '@server/lib/schedulers/update-videos-scheduler.js'
import { VideoChannelSyncLatestScheduler } from '@server/lib/schedulers/video-channel-sync-latest-scheduler.js'
import { VideoViewsBufferScheduler } from '@server/lib/schedulers/video-views-buffer-scheduler.js'
import { VideoViewsManager } from '@server/lib/views/video-views-manager.js'
import { authenticate, ensureUserHasRight } from '../../../middlewares/index.js'
import { RemoveExpiredUserExportsScheduler } from '@server/lib/schedulers/remove-expired-user-exports-scheduler.js'
const debugRouter = express.Router()
debugRouter.get('/debug',
authenticate,
ensureUserHasRight(UserRight.MANAGE_DEBUG),
getDebug
)
debugRouter.post('/debug/run-command',
authenticate,
ensureUserHasRight(UserRight.MANAGE_DEBUG),
runCommand
)
// ---------------------------------------------------------------------------
export {
debugRouter
}
// ---------------------------------------------------------------------------
function getDebug (req: express.Request, res: express.Response) {
return res.json({
ip: req.ip,
activityPubMessagesWaiting: InboxManager.Instance.getActivityPubMessagesWaiting()
} as Debug)
}
async function runCommand (req: express.Request, res: express.Response) {
const body: SendDebugCommand = req.body
const processors: { [id in SendDebugCommand['command']]: () => Promise<any> } = {
'remove-dandling-resumable-uploads': () => RemoveDanglingResumableUploadsScheduler.Instance.execute(),
'remove-expired-user-exports': () => RemoveExpiredUserExportsScheduler.Instance.execute(),
'process-video-views-buffer': () => VideoViewsBufferScheduler.Instance.execute(),
'process-video-viewers': () => VideoViewsManager.Instance.processViewerStats(),
'process-update-videos-scheduler': () => UpdateVideosScheduler.Instance.execute(),
'process-video-channel-sync-latest': () => VideoChannelSyncLatestScheduler.Instance.execute()
}
if (!processors[body.command]) {
return res.fail({ message: 'Invalid command' })
}
await processors[body.command]()
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
+210
ファイルの表示
@@ -0,0 +1,210 @@
import express from 'express'
import { HttpStatusCode, ServerFollowCreate, UserRight } from '@peertube/peertube-models'
import { getServerActor } from '@server/models/application/application.js'
import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow.js'
import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send/index.js'
import { JobQueue } from '../../../lib/job-queue/index.js'
import { removeRedundanciesOfServer } from '../../../lib/redundancy.js'
import {
asyncMiddleware,
authenticate,
ensureUserHasRight,
paginationValidator,
setBodyHostsPort,
setDefaultPagination,
setDefaultSort
} from '../../../middlewares/index.js'
import {
acceptFollowerValidator,
followValidator,
getFollowerValidator,
instanceFollowersSortValidator,
instanceFollowingSortValidator,
listFollowsValidator,
rejectFollowerValidator,
removeFollowingValidator
} from '../../../middlewares/validators/index.js'
import { ActorFollowModel } from '../../../models/actor/actor-follow.js'
const serverFollowsRouter = express.Router()
serverFollowsRouter.get('/following',
listFollowsValidator,
paginationValidator,
instanceFollowingSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listFollowing)
)
serverFollowsRouter.post('/following',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
followValidator,
setBodyHostsPort,
asyncMiddleware(addFollow)
)
serverFollowsRouter.delete('/following/:hostOrHandle',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
asyncMiddleware(removeFollowingValidator),
asyncMiddleware(removeFollowing)
)
serverFollowsRouter.get('/followers',
listFollowsValidator,
paginationValidator,
instanceFollowersSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listFollowers)
)
serverFollowsRouter.delete('/followers/:nameWithHost',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
asyncMiddleware(getFollowerValidator),
asyncMiddleware(removeFollower)
)
serverFollowsRouter.post('/followers/:nameWithHost/reject',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
asyncMiddleware(getFollowerValidator),
rejectFollowerValidator,
asyncMiddleware(rejectFollower)
)
serverFollowsRouter.post('/followers/:nameWithHost/accept',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
asyncMiddleware(getFollowerValidator),
acceptFollowerValidator,
asyncMiddleware(acceptFollower)
)
// ---------------------------------------------------------------------------
export {
serverFollowsRouter
}
// ---------------------------------------------------------------------------
async function listFollowing (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const resultList = await ActorFollowModel.listInstanceFollowingForApi({
followerId: serverActor.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
actorType: req.query.actorType,
state: req.query.state
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function listFollowers (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const resultList = await ActorFollowModel.listFollowersForApi({
actorIds: [ serverActor.id ],
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
actorType: req.query.actorType,
state: req.query.state
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function addFollow (req: express.Request, res: express.Response) {
const { hosts, handles } = req.body as ServerFollowCreate
const follower = await getServerActor()
for (const host of hosts) {
const payload = {
host,
followerActorId: follower.id
}
JobQueue.Instance.createJobAsync({ type: 'activitypub-follow', payload })
}
for (const handle of handles) {
const [ name, host ] = handle.split('@')
const payload = {
host,
name,
followerActorId: follower.id
}
JobQueue.Instance.createJobAsync({ type: 'activitypub-follow', payload })
}
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function removeFollowing (req: express.Request, res: express.Response) {
const follow = res.locals.follow
await sequelizeTypescript.transaction(async t => {
if (follow.state === 'accepted') sendUndoFollow(follow, t)
// Disable redundancy on unfollowed instances
const server = follow.ActorFollowing.Server
server.redundancyAllowed = false
await server.save({ transaction: t })
// Async, could be long
removeRedundanciesOfServer(server.id)
.catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err }))
await follow.destroy({ transaction: t })
})
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function rejectFollower (req: express.Request, res: express.Response) {
const follow = res.locals.follow
follow.state = 'rejected'
await follow.save()
sendReject(follow.url, follow.ActorFollower, follow.ActorFollowing)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function removeFollower (req: express.Request, res: express.Response) {
const follow = res.locals.follow
if (follow.state === 'accepted' || follow.state === 'pending') {
sendReject(follow.url, follow.ActorFollower, follow.ActorFollowing)
}
await follow.destroy()
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function acceptFollower (req: express.Request, res: express.Response) {
const follow = res.locals.follow
sendAccept(follow)
follow.state = 'accepted'
await follow.save()
await autoFollowBackIfNeeded(follow)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
+27
ファイルの表示
@@ -0,0 +1,27 @@
import express from 'express'
import { apiRateLimiter } from '@server/middlewares/index.js'
import { contactRouter } from './contact.js'
import { debugRouter } from './debug.js'
import { serverFollowsRouter } from './follows.js'
import { logsRouter } from './logs.js'
import { serverRedundancyRouter } from './redundancy.js'
import { serverBlocklistRouter } from './server-blocklist.js'
import { statsRouter } from './stats.js'
const serverRouter = express.Router()
serverRouter.use(apiRateLimiter)
serverRouter.use('/', serverFollowsRouter)
serverRouter.use('/', serverRedundancyRouter)
serverRouter.use('/', statsRouter)
serverRouter.use('/', serverBlocklistRouter)
serverRouter.use('/', contactRouter)
serverRouter.use('/', logsRouter)
serverRouter.use('/', debugRouter)
// ---------------------------------------------------------------------------
export {
serverRouter
}
+201
ファイルの表示
@@ -0,0 +1,201 @@
import express from 'express'
import { readdir, readFile } from 'fs/promises'
import { join } from 'path'
import { pick } from '@peertube/peertube-core-utils'
import { ClientLogCreate, HttpStatusCode, ServerLogLevel, UserRight } from '@peertube/peertube-models'
import { isArray } from '@server/helpers/custom-validators/misc.js'
import { logger, mtimeSortFilesDesc } from '@server/helpers/logger.js'
import { CONFIG } from '../../../initializers/config.js'
import { AUDIT_LOG_FILENAME, LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants.js'
import { asyncMiddleware, authenticate, buildRateLimiter, ensureUserHasRight, optionalAuthenticate } from '../../../middlewares/index.js'
import { createClientLogValidator, getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs.js'
const createClientLogRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.RECEIVE_CLIENT_LOG.WINDOW_MS,
max: CONFIG.RATES_LIMIT.RECEIVE_CLIENT_LOG.MAX
})
const logsRouter = express.Router()
logsRouter.post('/logs/client',
createClientLogRateLimiter,
optionalAuthenticate,
createClientLogValidator,
createClientLog
)
logsRouter.get('/logs',
authenticate,
ensureUserHasRight(UserRight.MANAGE_LOGS),
getLogsValidator,
asyncMiddleware(getLogs)
)
logsRouter.get('/audit-logs',
authenticate,
ensureUserHasRight(UserRight.MANAGE_LOGS),
getAuditLogsValidator,
asyncMiddleware(getAuditLogs)
)
// ---------------------------------------------------------------------------
export {
logsRouter
}
// ---------------------------------------------------------------------------
function createClientLog (req: express.Request, res: express.Response) {
const logInfo = req.body as ClientLogCreate
const meta = {
tags: [ 'client' ],
username: res.locals.oauth?.token?.User?.username,
...pick(logInfo, [ 'userAgent', 'stackTrace', 'meta', 'url' ])
}
logger.log(logInfo.level, `Client log: ${logInfo.message}`, meta)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
const auditLogNameFilter = generateLogNameFilter(AUDIT_LOG_FILENAME)
async function getAuditLogs (req: express.Request, res: express.Response) {
const output = await generateOutput({
startDateQuery: req.query.startDate,
endDateQuery: req.query.endDate,
level: 'audit',
nameFilter: auditLogNameFilter
})
return res.json(output).end()
}
const logNameFilter = generateLogNameFilter(LOG_FILENAME)
async function getLogs (req: express.Request, res: express.Response) {
const output = await generateOutput({
startDateQuery: req.query.startDate,
endDateQuery: req.query.endDate,
level: req.query.level || 'info',
tagsOneOf: req.query.tagsOneOf,
nameFilter: logNameFilter
})
return res.json(output)
}
async function generateOutput (options: {
startDateQuery: string
endDateQuery?: string
level: ServerLogLevel
nameFilter: RegExp
tagsOneOf?: string[]
}) {
const { startDateQuery, level, nameFilter } = options
const tagsOneOf = Array.isArray(options.tagsOneOf) && options.tagsOneOf.length !== 0
? new Set(options.tagsOneOf)
: undefined
const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR)
const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR)
let currentSize = 0
const startDate = new Date(startDateQuery)
const endDate = options.endDateQuery ? new Date(options.endDateQuery) : new Date()
let output: string[] = []
for (const meta of sortedLogFiles) {
if (nameFilter.exec(meta.file) === null) continue
const path = join(CONFIG.STORAGE.LOG_DIR, meta.file)
logger.debug('Opening %s to fetch logs.', path)
const result = await getOutputFromFile({ path, startDate, endDate, level, currentSize, tagsOneOf })
if (!result.output) break
output = result.output.concat(output)
currentSize = result.currentSize
if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break
}
return output
}
async function getOutputFromFile (options: {
path: string
startDate: Date
endDate: Date
level: ServerLogLevel
currentSize: number
tagsOneOf: Set<string>
}) {
const { path, startDate, endDate, level, tagsOneOf } = options
const startTime = startDate.getTime()
const endTime = endDate.getTime()
let currentSize = options.currentSize
let logTime: number
const logsLevel: { [ id in ServerLogLevel ]: number } = {
audit: -1,
debug: 0,
info: 1,
warn: 2,
error: 3
}
const content = await readFile(path)
const lines = content.toString().split('\n')
const output: any[] = []
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i]
let log: any
try {
log = JSON.parse(line)
} catch {
// Maybe there a multiple \n at the end of the file
continue
}
logTime = new Date(log.timestamp).getTime()
if (
logTime >= startTime &&
logTime <= endTime &&
logsLevel[log.level] >= logsLevel[level] &&
(!tagsOneOf || lineHasTag(log, tagsOneOf))
) {
output.push(log)
currentSize += line.length
if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) break
} else if (logTime < startTime) {
break
}
}
return { currentSize, output: output.reverse(), logTime }
}
function lineHasTag (line: { tags?: string }, tagsOneOf: Set<string>) {
if (!isArray(line.tags)) return false
for (const lineTag of line.tags) {
if (tagsOneOf.has(lineTag)) return true
}
return false
}
function generateLogNameFilter (baseName: string) {
return new RegExp('^' + baseName.replace(/\.log$/, '') + '\\d*.log$')
}
+115
ファイルの表示
@@ -0,0 +1,115 @@
import express from 'express'
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
import { JobQueue } from '@server/lib/job-queue/index.js'
import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy.js'
import { logger } from '../../../helpers/logger.js'
import { removeRedundanciesOfServer, removeVideoRedundancy } from '../../../lib/redundancy.js'
import {
asyncMiddleware,
authenticate,
ensureUserHasRight,
paginationValidator,
setDefaultPagination,
setDefaultVideoRedundanciesSort,
videoRedundanciesSortValidator
} from '../../../middlewares/index.js'
import {
addVideoRedundancyValidator,
listVideoRedundanciesValidator,
removeVideoRedundancyValidator,
updateServerRedundancyValidator
} from '../../../middlewares/validators/redundancy.js'
const serverRedundancyRouter = express.Router()
serverRedundancyRouter.put('/redundancy/:host',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
asyncMiddleware(updateServerRedundancyValidator),
asyncMiddleware(updateRedundancy)
)
serverRedundancyRouter.get('/redundancy/videos',
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES),
listVideoRedundanciesValidator,
paginationValidator,
videoRedundanciesSortValidator,
setDefaultVideoRedundanciesSort,
setDefaultPagination,
asyncMiddleware(listVideoRedundancies)
)
serverRedundancyRouter.post('/redundancy/videos',
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES),
addVideoRedundancyValidator,
asyncMiddleware(addVideoRedundancy)
)
serverRedundancyRouter.delete('/redundancy/videos/:redundancyId',
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES),
removeVideoRedundancyValidator,
asyncMiddleware(removeVideoRedundancyController)
)
// ---------------------------------------------------------------------------
export {
serverRedundancyRouter
}
// ---------------------------------------------------------------------------
async function listVideoRedundancies (req: express.Request, res: express.Response) {
const resultList = await VideoRedundancyModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
target: req.query.target,
strategy: req.query.strategy
})
const result = {
total: resultList.total,
data: resultList.data.map(r => VideoRedundancyModel.toFormattedJSONStatic(r))
}
return res.json(result)
}
async function addVideoRedundancy (req: express.Request, res: express.Response) {
const payload = {
videoId: res.locals.onlyVideo.id
}
await JobQueue.Instance.createJob({
type: 'video-redundancy',
payload
})
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function removeVideoRedundancyController (req: express.Request, res: express.Response) {
await removeVideoRedundancy(res.locals.videoRedundancy)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function updateRedundancy (req: express.Request, res: express.Response) {
const server = res.locals.server
server.redundancyAllowed = req.body.redundancyAllowed
await server.save()
if (server.redundancyAllowed !== true) {
// Async, could be long
removeRedundanciesOfServer(server.id)
.catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err }))
}
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
+152
ファイルの表示
@@ -0,0 +1,152 @@
import 'multer'
import express from 'express'
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
import { getServerActor } from '@server/models/application/application.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import {
addAccountInBlocklist,
addServerInBlocklist,
removeAccountFromBlocklist,
removeServerFromBlocklist
} from '../../../lib/blocklist.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
ensureUserHasRight,
paginationValidator,
setDefaultPagination,
setDefaultSort
} from '../../../middlewares/index.js'
import {
accountsBlocklistSortValidator,
blockAccountValidator,
blockServerValidator,
serversBlocklistSortValidator,
unblockAccountByServerValidator,
unblockServerByServerValidator
} from '../../../middlewares/validators/index.js'
import { AccountBlocklistModel } from '../../../models/account/account-blocklist.js'
import { ServerBlocklistModel } from '../../../models/server/server-blocklist.js'
const serverBlocklistRouter = express.Router()
serverBlocklistRouter.get('/blocklist/accounts',
authenticate,
ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
paginationValidator,
accountsBlocklistSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listBlockedAccounts)
)
serverBlocklistRouter.post('/blocklist/accounts',
authenticate,
ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
asyncMiddleware(blockAccountValidator),
asyncRetryTransactionMiddleware(blockAccount)
)
serverBlocklistRouter.delete('/blocklist/accounts/:accountName',
authenticate,
ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
asyncMiddleware(unblockAccountByServerValidator),
asyncRetryTransactionMiddleware(unblockAccount)
)
serverBlocklistRouter.get('/blocklist/servers',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
paginationValidator,
serversBlocklistSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listBlockedServers)
)
serverBlocklistRouter.post('/blocklist/servers',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
asyncMiddleware(blockServerValidator),
asyncRetryTransactionMiddleware(blockServer)
)
serverBlocklistRouter.delete('/blocklist/servers/:host',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
asyncMiddleware(unblockServerByServerValidator),
asyncRetryTransactionMiddleware(unblockServer)
)
export {
serverBlocklistRouter
}
// ---------------------------------------------------------------------------
async function listBlockedAccounts (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const resultList = await AccountBlocklistModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
accountId: serverActor.Account.id
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function blockAccount (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const accountToBlock = res.locals.account
await addAccountInBlocklist({ byAccountId: serverActor.Account.id, targetAccountId: accountToBlock.id, removeNotificationOfUserId: null })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function unblockAccount (req: express.Request, res: express.Response) {
const accountBlock = res.locals.accountBlock
await removeAccountFromBlocklist(accountBlock)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function listBlockedServers (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const resultList = await ServerBlocklistModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
accountId: serverActor.Account.id
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function blockServer (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const serverToBlock = res.locals.server
await addServerInBlocklist({
byAccountId: serverActor.Account.id,
targetServerId: serverToBlock.id,
removeNotificationOfUserId: null
})
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function unblockServer (req: express.Request, res: express.Response) {
const serverBlock = res.locals.serverBlock
await removeServerFromBlocklist(serverBlock)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+26
ファイルの表示
@@ -0,0 +1,26 @@
import express from 'express'
import { StatsManager } from '@server/lib/stat-manager.js'
import { ROUTE_CACHE_LIFETIME } from '../../../initializers/constants.js'
import { asyncMiddleware } from '../../../middlewares/index.js'
import { cacheRoute } from '../../../middlewares/cache/cache.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
const statsRouter = express.Router()
statsRouter.get('/stats',
cacheRoute(ROUTE_CACHE_LIFETIME.STATS),
asyncMiddleware(getStats)
)
async function getStats (_req: express.Request, res: express.Response) {
let data = await StatsManager.Instance.getStats()
data = await Hooks.wrapObject(data, 'filter:api.server.stats.get.result')
return res.json(data)
}
// ---------------------------------------------------------------------------
export {
statsRouter
}
+72
ファイルの表示
@@ -0,0 +1,72 @@
import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { CONFIG } from '../../../initializers/config.js'
import { sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user.js'
import { asyncMiddleware, buildRateLimiter } from '../../../middlewares/index.js'
import {
registrationVerifyEmailValidator,
usersAskSendVerifyEmailValidator,
usersVerifyEmailValidator
} from '../../../middlewares/validators/index.js'
const askSendEmailLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
})
const emailVerificationRouter = express.Router()
emailVerificationRouter.post([ '/ask-send-verify-email', '/registrations/ask-send-verify-email' ],
askSendEmailLimiter,
asyncMiddleware(usersAskSendVerifyEmailValidator),
asyncMiddleware(reSendVerifyUserEmail)
)
emailVerificationRouter.post('/:id/verify-email',
asyncMiddleware(usersVerifyEmailValidator),
asyncMiddleware(verifyUserEmail)
)
emailVerificationRouter.post('/registrations/:registrationId/verify-email',
asyncMiddleware(registrationVerifyEmailValidator),
asyncMiddleware(verifyRegistrationEmail)
)
// ---------------------------------------------------------------------------
export {
emailVerificationRouter
}
async function reSendVerifyUserEmail (req: express.Request, res: express.Response) {
const user = res.locals.user
const registration = res.locals.userRegistration
if (user) await sendVerifyUserEmail(user)
else if (registration) await sendVerifyRegistrationEmail(registration)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function verifyUserEmail (req: express.Request, res: express.Response) {
const user = res.locals.user
user.emailVerified = true
if (req.body.isPendingEmail === true) {
user.email = user.pendingEmail
user.pendingEmail = null
}
await user.save()
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function verifyRegistrationEmail (req: express.Request, res: express.Response) {
const registration = res.locals.userRegistration
registration.emailVerified = true
await registration.save()
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
+323
ファイルの表示
@@ -0,0 +1,323 @@
import express from 'express'
import { tokensRouter } from '@server/controllers/api/users/token.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { OAuthTokenModel } from '@server/models/oauth/oauth-token.js'
import { MUserAccountDefault } from '@server/types/models/index.js'
import { pick } from '@peertube/peertube-core-utils'
import { HttpStatusCode, UserCreate, UserCreateResult, UserRight, UserUpdate } from '@peertube/peertube-models'
import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger.js'
import { logger } from '../../../helpers/logger.js'
import { generateRandomString, getFormattedObjects } from '../../../helpers/utils.js'
import { WEBSERVER } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { Emailer } from '../../../lib/emailer.js'
import { Redis } from '../../../lib/redis.js'
import { buildUser, createUserAccountAndChannelAndPlaylist } from '../../../lib/user.js'
import {
adminUsersSortValidator,
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
ensureUserHasRight,
paginationValidator,
setDefaultPagination,
setDefaultSort,
userAutocompleteValidator,
usersAddValidator,
usersGetValidator,
usersListValidator,
usersRemoveValidator,
usersUpdateValidator
} from '../../../middlewares/index.js'
import {
ensureCanModerateUser,
usersAskResetPasswordValidator,
usersBlockingValidator,
usersResetPasswordValidator
} from '../../../middlewares/validators/index.js'
import { UserModel } from '../../../models/user/user.js'
import { emailVerificationRouter } from './email-verification.js'
import { meRouter } from './me.js'
import { myAbusesRouter } from './my-abuses.js'
import { myBlocklistRouter } from './my-blocklist.js'
import { myVideosHistoryRouter } from './my-history.js'
import { myNotificationsRouter } from './my-notifications.js'
import { mySubscriptionsRouter } from './my-subscriptions.js'
import { myVideoPlaylistsRouter } from './my-video-playlists.js'
import { registrationsRouter } from './registrations.js'
import { twoFactorRouter } from './two-factor.js'
import { userExportsRouter } from './user-exports.js'
import { userImportRouter } from './user-imports.js'
const auditLogger = auditLoggerFactory('users')
const usersRouter = express.Router()
usersRouter.use(apiRateLimiter)
usersRouter.use('/', emailVerificationRouter)
usersRouter.use('/', userExportsRouter)
usersRouter.use('/', userImportRouter)
usersRouter.use('/', registrationsRouter)
usersRouter.use('/', twoFactorRouter)
usersRouter.use('/', tokensRouter)
usersRouter.use('/', myNotificationsRouter)
usersRouter.use('/', mySubscriptionsRouter)
usersRouter.use('/', myBlocklistRouter)
usersRouter.use('/', myVideosHistoryRouter)
usersRouter.use('/', myVideoPlaylistsRouter)
usersRouter.use('/', myAbusesRouter)
usersRouter.use('/', meRouter)
usersRouter.get('/autocomplete',
userAutocompleteValidator,
asyncMiddleware(autocompleteUsers)
)
usersRouter.get('/',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
paginationValidator,
adminUsersSortValidator,
setDefaultSort,
setDefaultPagination,
usersListValidator,
asyncMiddleware(listUsers)
)
usersRouter.post('/:id/block',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(usersBlockingValidator),
ensureCanModerateUser,
asyncMiddleware(blockUser)
)
usersRouter.post('/:id/unblock',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(usersBlockingValidator),
ensureCanModerateUser,
asyncMiddleware(unblockUser)
)
usersRouter.get('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(usersGetValidator),
getUser
)
usersRouter.post('/',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(usersAddValidator),
asyncRetryTransactionMiddleware(createUser)
)
usersRouter.put('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(usersUpdateValidator),
ensureCanModerateUser,
asyncMiddleware(updateUser)
)
usersRouter.delete('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(usersRemoveValidator),
ensureCanModerateUser,
asyncMiddleware(removeUser)
)
usersRouter.post('/ask-reset-password',
asyncMiddleware(usersAskResetPasswordValidator),
asyncMiddleware(askResetUserPassword)
)
usersRouter.post('/:id/reset-password',
asyncMiddleware(usersResetPasswordValidator),
asyncMiddleware(resetUserPassword)
)
// ---------------------------------------------------------------------------
export {
usersRouter
}
// ---------------------------------------------------------------------------
async function createUser (req: express.Request, res: express.Response) {
const body: UserCreate = req.body
const userToCreate = buildUser({
...pick(body, [ 'username', 'password', 'email', 'role', 'videoQuota', 'videoQuotaDaily', 'adminFlags' ]),
emailVerified: null
})
// NB: due to the validator usersAddValidator, password==='' can only be true if we can send the mail.
const createPassword = userToCreate.password === ''
if (createPassword) {
userToCreate.password = await generateRandomString(20)
}
const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({
userToCreate,
channelNames: body.channelName && { name: body.channelName, displayName: body.channelName }
})
auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
logger.info('User %s with its channel and account created.', body.username)
if (createPassword) {
// this will send an email for newly created users, so then can set their first password.
logger.info('Sending to user %s a create password email', body.username)
const verificationString = await Redis.Instance.setCreatePasswordVerificationString(user.id)
const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
Emailer.Instance.addPasswordCreateEmailJob(userToCreate.username, user.email, url)
}
Hooks.runAction('action:api.user.created', { body, user, account, videoChannel, req, res })
return res.json({
user: {
id: user.id,
account: {
id: account.id
}
} as UserCreateResult
})
}
async function unblockUser (req: express.Request, res: express.Response) {
const user = res.locals.user
await changeUserBlock(res, user, false)
Hooks.runAction('action:api.user.unblocked', { user, req, res })
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function blockUser (req: express.Request, res: express.Response) {
const user = res.locals.user
const reason = req.body.reason
await changeUserBlock(res, user, true, reason)
Hooks.runAction('action:api.user.blocked', { user, req, res })
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
function getUser (req: express.Request, res: express.Response) {
return res.json(res.locals.user.toFormattedJSON({ withAdminFlags: true }))
}
async function autocompleteUsers (req: express.Request, res: express.Response) {
const resultList = await UserModel.autoComplete(req.query.search as string)
return res.json(resultList)
}
async function listUsers (req: express.Request, res: express.Response) {
const resultList = await UserModel.listForAdminApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
blocked: req.query.blocked
})
return res.json(getFormattedObjects(resultList.data, resultList.total, { withAdminFlags: true }))
}
async function removeUser (req: express.Request, res: express.Response) {
const user = res.locals.user
auditLogger.delete(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
await sequelizeTypescript.transaction(async t => {
// Use a transaction to avoid inconsistencies with hooks (account/channel deletion & federation)
await user.destroy({ transaction: t })
})
Hooks.runAction('action:api.user.deleted', { user, req, res })
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function updateUser (req: express.Request, res: express.Response) {
const body: UserUpdate = req.body
const userToUpdate = res.locals.user
const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON())
const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
const keysToUpdate: (keyof UserUpdate)[] = [
'password',
'email',
'emailVerified',
'videoQuota',
'videoQuotaDaily',
'role',
'adminFlags',
'pluginAuth'
]
for (const key of keysToUpdate) {
if (body[key] !== undefined) userToUpdate.set(key, body[key])
}
const user = await userToUpdate.save()
// Destroy user token to refresh rights
if (roleChanged || body.password !== undefined) await OAuthTokenModel.deleteUserToken(userToUpdate.id)
auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
Hooks.runAction('action:api.user.updated', { user, req, res })
// Don't need to send this update to followers, these attributes are not federated
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function askResetUserPassword (req: express.Request, res: express.Response) {
const user = res.locals.user
const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id)
const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
Emailer.Instance.addPasswordResetEmailJob(user.username, user.email, url)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function resetUserPassword (req: express.Request, res: express.Response) {
const user = res.locals.user
user.password = req.body.password
await user.save()
await Redis.Instance.removePasswordVerificationString(user.id)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) {
const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
user.blocked = block
user.blockedReason = reason || null
await sequelizeTypescript.transaction(async t => {
await OAuthTokenModel.deleteUserToken(user.id, t)
await user.save({ transaction: t })
})
Emailer.Instance.addUserBlockJob(user, block, reason)
auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
}
+332
ファイルの表示
@@ -0,0 +1,332 @@
import { pick } from '@peertube/peertube-core-utils'
import {
ActorImageType,
UserVideoRate as FormattedUserVideoRate,
HttpStatusCode,
UserUpdateMe,
UserVideoQuota
} from '@peertube/peertube-models'
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
import { UserAuditView, auditLoggerFactory, getAuditIdFromRes } from '@server/helpers/audit-logger.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { VideoCommentModel } from '@server/models/video/video-comment.js'
import express from 'express'
import 'multer'
import { createReqFiles } from '../../../helpers/express-utils.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { CONFIG } from '../../../initializers/config.js'
import { MIMETYPES } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { sendUpdateActor } from '../../../lib/activitypub/send/index.js'
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor.js'
import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort,
setDefaultVideosSort,
usersUpdateMeValidator,
usersVideoRatingValidator
} from '../../../middlewares/index.js'
import { updateAvatarValidator } from '../../../middlewares/validators/actor-image.js'
import {
deleteMeValidator,
getMyVideoImportsValidator,
listCommentsOnUserVideosValidator,
usersVideosValidator,
videoImportsSortValidator,
videosSortValidator
} from '../../../middlewares/validators/index.js'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
import { AccountModel } from '../../../models/account/account.js'
import { UserModel } from '../../../models/user/user.js'
import { VideoImportModel } from '../../../models/video/video-import.js'
import { VideoModel } from '../../../models/video/video.js'
const auditLogger = auditLoggerFactory('users')
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
const meRouter = express.Router()
meRouter.get('/me',
authenticate,
asyncMiddleware(getUserInformation)
)
meRouter.delete('/me',
authenticate,
deleteMeValidator,
asyncMiddleware(deleteMe)
)
meRouter.get('/me/video-quota-used',
authenticate,
asyncMiddleware(getUserVideoQuotaUsed)
)
meRouter.get('/me/videos/imports',
authenticate,
paginationValidator,
videoImportsSortValidator,
setDefaultSort,
setDefaultPagination,
getMyVideoImportsValidator,
asyncMiddleware(getUserVideoImports)
)
meRouter.get('/me/videos/comments',
authenticate,
paginationValidator,
videosSortValidator,
setDefaultVideosSort,
setDefaultPagination,
asyncMiddleware(listCommentsOnUserVideosValidator),
asyncMiddleware(listCommentsOnUserVideos)
)
meRouter.get('/me/videos',
authenticate,
paginationValidator,
videosSortValidator,
setDefaultVideosSort,
setDefaultPagination,
asyncMiddleware(usersVideosValidator),
asyncMiddleware(listUserVideos)
)
meRouter.get('/me/videos/:videoId/rating',
authenticate,
asyncMiddleware(usersVideoRatingValidator),
asyncMiddleware(getUserVideoRating)
)
meRouter.put('/me',
authenticate,
asyncMiddleware(usersUpdateMeValidator),
asyncRetryTransactionMiddleware(updateMe)
)
meRouter.post('/me/avatar/pick',
authenticate,
reqAvatarFile,
updateAvatarValidator,
asyncRetryTransactionMiddleware(updateMyAvatar)
)
meRouter.delete('/me/avatar',
authenticate,
asyncRetryTransactionMiddleware(deleteMyAvatar)
)
// ---------------------------------------------------------------------------
export {
meRouter
}
// ---------------------------------------------------------------------------
async function listUserVideos (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const apiOptions = await Hooks.wrapObject({
accountId: user.Account.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
channelId: res.locals.videoChannel?.id,
isLive: req.query.isLive
}, 'filter:api.user.me.videos.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoModel.listUserVideosForApi.bind(VideoModel),
apiOptions,
'filter:api.user.me.videos.list.result'
)
const additionalAttributes = {
waitTranscoding: true,
state: true,
scheduledUpdate: true,
blacklistInfo: true
}
return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
}
async function listCommentsOnUserVideos (req: express.Request, res: express.Response) {
const userAccount = res.locals.oauth.token.User.Account
const options = {
...pick(req.query, [
'start',
'count',
'sort',
'search',
'searchAccount',
'searchVideo',
'autoTagOneOf'
]),
autoTagOfAccountId: userAccount.id,
videoAccountOwnerId: userAccount.id,
heldForReview: req.query.isHeldForReview,
videoChannelOwnerId: res.locals.videoChannel?.id,
videoId: res.locals.videoAll?.id
}
const resultList = await VideoCommentModel.listCommentsForApi(options)
return res.json({
total: resultList.total,
data: resultList.data.map(c => c.toFormattedForAdminOrUserJSON())
})
}
async function getUserVideoImports (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await VideoImportModel.listUserVideoImportsForApi({
userId: user.id,
...pick(req.query, [ 'targetUrl', 'start', 'count', 'sort', 'search', 'videoChannelSyncId' ])
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function getUserInformation (req: express.Request, res: express.Response) {
// We did not load channels in res.locals.user
const user = await UserModel.loadForMeAPI(res.locals.oauth.token.user.id)
const result = await Hooks.wrapObject(
user.toMeFormattedJSON(),
'filter:api.user.me.get.result',
{ user }
)
return res.json(result)
}
async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user
const videoQuotaUsed = await getOriginalVideoFileTotalFromUser(user)
const videoQuotaUsedDaily = await getOriginalVideoFileTotalDailyFromUser(user)
const data: UserVideoQuota = {
videoQuotaUsed,
videoQuotaUsedDaily
}
return res.json(data)
}
async function getUserVideoRating (req: express.Request, res: express.Response) {
const videoId = res.locals.videoId.id
const accountId = +res.locals.oauth.token.User.Account.id
const ratingObj = await AccountVideoRateModel.load(accountId, videoId, null)
const rating = ratingObj ? ratingObj.type : 'none'
const json: FormattedUserVideoRate = {
videoId,
rating
}
return res.json(json)
}
async function deleteMe (req: express.Request, res: express.Response) {
const user = await UserModel.loadByIdWithChannels(res.locals.oauth.token.User.id)
auditLogger.delete(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
await user.destroy()
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function updateMe (req: express.Request, res: express.Response) {
const body: UserUpdateMe = req.body
let sendVerificationEmail = false
const user = res.locals.oauth.token.user
const keysToUpdate: (keyof UserUpdateMe & keyof AttributesOnly<UserModel>)[] = [
'password',
'nsfwPolicy',
'p2pEnabled',
'autoPlayVideo',
'autoPlayNextVideo',
'autoPlayNextVideoPlaylist',
'videosHistoryEnabled',
'videoLanguages',
'theme',
'noInstanceConfigWarningModal',
'noAccountSetupWarningModal',
'noWelcomeModal',
'emailPublic',
'p2pEnabled'
]
for (const key of keysToUpdate) {
if (body[key] !== undefined) user.set(key, body[key])
}
if (body.email !== undefined) {
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
user.pendingEmail = body.email
sendVerificationEmail = true
} else {
user.email = body.email
}
}
await sequelizeTypescript.transaction(async t => {
await user.save({ transaction: t })
if (body.displayName === undefined && body.description === undefined) return
const userAccount = await AccountModel.load(user.Account.id, t)
if (body.displayName !== undefined) userAccount.name = body.displayName
if (body.description !== undefined) userAccount.description = body.description
await userAccount.save({ transaction: t })
await sendUpdateActor(userAccount, t)
})
if (sendVerificationEmail === true) {
await sendVerifyUserEmail(user, true)
}
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function updateMyAvatar (req: express.Request, res: express.Response) {
const avatarPhysicalFile = req.files['avatarfile'][0]
const user = res.locals.oauth.token.user
const userAccount = await AccountModel.load(user.Account.id)
const avatars = await updateLocalActorImageFiles({
accountOrChannel: userAccount,
imagePhysicalFile: avatarPhysicalFile,
type: ActorImageType.AVATAR,
sendActorUpdate: true
})
return res.json({
avatars: avatars.map(avatar => avatar.toFormattedJSON())
})
}
async function deleteMyAvatar (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user
const userAccount = await AccountModel.load(user.Account.id)
await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR)
return res.json({ avatars: [] })
}
+48
ファイルの表示
@@ -0,0 +1,48 @@
import express from 'express'
import { AbuseModel } from '@server/models/abuse/abuse.js'
import {
abuseListForUserValidator,
abusesSortValidator,
asyncMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort
} from '../../../middlewares/index.js'
const myAbusesRouter = express.Router()
myAbusesRouter.get('/me/abuses',
authenticate,
paginationValidator,
abusesSortValidator,
setDefaultSort,
setDefaultPagination,
abuseListForUserValidator,
asyncMiddleware(listMyAbuses)
)
// ---------------------------------------------------------------------------
export {
myAbusesRouter
}
// ---------------------------------------------------------------------------
async function listMyAbuses (req: express.Request, res: express.Response) {
const resultList = await AbuseModel.listForUserApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
id: req.query.id,
search: req.query.search,
state: req.query.state,
user: res.locals.oauth.token.User
})
return res.json({
total: resultList.total,
data: resultList.data.map(d => d.toFormattedUserJSON())
})
}
+144
ファイルの表示
@@ -0,0 +1,144 @@
import 'multer'
import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { getFormattedObjects } from '../../../helpers/utils.js'
import {
addAccountInBlocklist,
addServerInBlocklist,
removeAccountFromBlocklist,
removeServerFromBlocklist
} from '../../../lib/blocklist.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort,
unblockAccountByAccountValidator
} from '../../../middlewares/index.js'
import {
accountsBlocklistSortValidator,
blockAccountValidator,
blockServerValidator,
serversBlocklistSortValidator,
unblockServerByAccountValidator
} from '../../../middlewares/validators/index.js'
import { AccountBlocklistModel } from '../../../models/account/account-blocklist.js'
import { ServerBlocklistModel } from '../../../models/server/server-blocklist.js'
const myBlocklistRouter = express.Router()
myBlocklistRouter.get('/me/blocklist/accounts',
authenticate,
paginationValidator,
accountsBlocklistSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listBlockedAccounts)
)
myBlocklistRouter.post('/me/blocklist/accounts',
authenticate,
asyncMiddleware(blockAccountValidator),
asyncRetryTransactionMiddleware(blockAccount)
)
myBlocklistRouter.delete('/me/blocklist/accounts/:accountName',
authenticate,
asyncMiddleware(unblockAccountByAccountValidator),
asyncRetryTransactionMiddleware(unblockAccount)
)
myBlocklistRouter.get('/me/blocklist/servers',
authenticate,
paginationValidator,
serversBlocklistSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listBlockedServers)
)
myBlocklistRouter.post('/me/blocklist/servers',
authenticate,
asyncMiddleware(blockServerValidator),
asyncRetryTransactionMiddleware(blockServer)
)
myBlocklistRouter.delete('/me/blocklist/servers/:host',
authenticate,
asyncMiddleware(unblockServerByAccountValidator),
asyncRetryTransactionMiddleware(unblockServer)
)
export {
myBlocklistRouter
}
// ---------------------------------------------------------------------------
async function listBlockedAccounts (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await AccountBlocklistModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
accountId: user.Account.id
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function blockAccount (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const accountToBlock = res.locals.account
await addAccountInBlocklist({ byAccountId: user.Account.id, targetAccountId: accountToBlock.id, removeNotificationOfUserId: user.id })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function unblockAccount (req: express.Request, res: express.Response) {
const accountBlock = res.locals.accountBlock
await removeAccountFromBlocklist(accountBlock)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function listBlockedServers (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await ServerBlocklistModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
accountId: user.Account.id
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function blockServer (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const serverToBlock = res.locals.server
await addServerInBlocklist({
byAccountId: user.Account.id,
targetServerId: serverToBlock.id,
removeNotificationOfUserId: user.id
})
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function unblockServer (req: express.Request, res: express.Response) {
const serverBlock = res.locals.serverBlock
await removeServerFromBlocklist(serverBlock)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+75
ファイルの表示
@@ -0,0 +1,75 @@
import express from 'express'
import { forceNumber } from '@peertube/peertube-core-utils'
import { HttpStatusCode } from '@peertube/peertube-models'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
userHistoryListValidator,
userHistoryRemoveAllValidator,
userHistoryRemoveElementValidator
} from '../../../middlewares/index.js'
import { UserVideoHistoryModel } from '../../../models/user/user-video-history.js'
const myVideosHistoryRouter = express.Router()
myVideosHistoryRouter.get('/me/history/videos',
authenticate,
paginationValidator,
setDefaultPagination,
userHistoryListValidator,
asyncMiddleware(listMyVideosHistory)
)
myVideosHistoryRouter.delete('/me/history/videos/:videoId',
authenticate,
userHistoryRemoveElementValidator,
asyncMiddleware(removeUserHistoryElement)
)
myVideosHistoryRouter.post('/me/history/videos/remove',
authenticate,
userHistoryRemoveAllValidator,
asyncRetryTransactionMiddleware(removeAllUserHistory)
)
// ---------------------------------------------------------------------------
export {
myVideosHistoryRouter
}
// ---------------------------------------------------------------------------
async function listMyVideosHistory (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count, req.query.search)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function removeUserHistoryElement (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
await UserVideoHistoryModel.removeUserHistoryElement(user, forceNumber(req.params.videoId))
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function removeAllUserHistory (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const beforeDate = req.body.beforeDate || null
await sequelizeTypescript.transaction(t => {
return UserVideoHistoryModel.removeUserHistoryBefore(user, beforeDate, t)
})
return res.type('json')
.status(HttpStatusCode.NO_CONTENT_204)
.end()
}
+110
ファイルの表示
@@ -0,0 +1,110 @@
import 'multer'
import express from 'express'
import { HttpStatusCode, UserNotificationSetting } from '@peertube/peertube-models'
import { getFormattedObjects } from '@server/helpers/utils.js'
import { UserNotificationModel } from '@server/models/user/user-notification.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort,
userNotificationsSortValidator
} from '../../../middlewares/index.js'
import {
listUserNotificationsValidator,
markAsReadUserNotificationsValidator,
updateNotificationSettingsValidator
} from '../../../middlewares/validators/users/user-notifications.js'
import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting.js'
import { meRouter } from './me.js'
const myNotificationsRouter = express.Router()
meRouter.put('/me/notification-settings',
authenticate,
updateNotificationSettingsValidator,
asyncRetryTransactionMiddleware(updateNotificationSettings)
)
myNotificationsRouter.get('/me/notifications',
authenticate,
paginationValidator,
userNotificationsSortValidator,
setDefaultSort,
setDefaultPagination,
listUserNotificationsValidator,
asyncMiddleware(listUserNotifications)
)
myNotificationsRouter.post('/me/notifications/read',
authenticate,
markAsReadUserNotificationsValidator,
asyncMiddleware(markAsReadUserNotifications)
)
myNotificationsRouter.post('/me/notifications/read-all',
authenticate,
asyncMiddleware(markAsReadAllUserNotifications)
)
export {
myNotificationsRouter
}
// ---------------------------------------------------------------------------
async function updateNotificationSettings (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const body = req.body as UserNotificationSetting
const values: UserNotificationSetting = {
newVideoFromSubscription: body.newVideoFromSubscription,
newCommentOnMyVideo: body.newCommentOnMyVideo,
abuseAsModerator: body.abuseAsModerator,
videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
blacklistOnMyVideo: body.blacklistOnMyVideo,
myVideoPublished: body.myVideoPublished,
myVideoImportFinished: body.myVideoImportFinished,
newFollow: body.newFollow,
newUserRegistration: body.newUserRegistration,
commentMention: body.commentMention,
newInstanceFollower: body.newInstanceFollower,
autoInstanceFollowing: body.autoInstanceFollowing,
abuseNewMessage: body.abuseNewMessage,
abuseStateChange: body.abuseStateChange,
newPeerTubeVersion: body.newPeerTubeVersion,
newPluginVersion: body.newPluginVersion,
myVideoTranscriptionGenerated: body.myVideoTranscriptionGenerated,
myVideoStudioEditionFinished: body.myVideoStudioEditionFinished
}
await UserNotificationSettingModel.updateUserSettings(values, user.id)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function listUserNotifications (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort, req.query.unread)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function markAsReadUserNotifications (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
await UserNotificationModel.markAsRead(user.id, req.body.ids)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function markAsReadAllUserNotifications (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
await UserNotificationModel.markAllAsRead(user.id)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+193
ファイルの表示
@@ -0,0 +1,193 @@
import 'multer'
import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { handlesToNameAndHost } from '@server/helpers/actors.js'
import { pickCommonVideoQuery } from '@server/helpers/query.js'
import { sendUndoFollow } from '@server/lib/activitypub/send/index.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { VideoChannelModel } from '@server/models/video/video-channel.js'
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { JobQueue } from '../../../lib/job-queue/index.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
commonVideosFiltersValidator,
paginationValidator,
setDefaultPagination,
setDefaultSort,
setDefaultVideosSort,
userSubscriptionAddValidator,
userSubscriptionGetValidator
} from '../../../middlewares/index.js'
import {
areSubscriptionsExistValidator,
userSubscriptionListValidator,
userSubscriptionsSortValidator,
videosSortValidator
} from '../../../middlewares/validators/index.js'
import { ActorFollowModel } from '../../../models/actor/actor-follow.js'
import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter/index.js'
import { VideoModel } from '../../../models/video/video.js'
const mySubscriptionsRouter = express.Router()
mySubscriptionsRouter.get('/me/subscriptions/videos',
authenticate,
paginationValidator,
videosSortValidator,
setDefaultVideosSort,
setDefaultPagination,
commonVideosFiltersValidator,
asyncMiddleware(getUserSubscriptionVideos)
)
mySubscriptionsRouter.get('/me/subscriptions/exist',
authenticate,
areSubscriptionsExistValidator,
asyncMiddleware(areSubscriptionsExist)
)
mySubscriptionsRouter.get('/me/subscriptions',
authenticate,
paginationValidator,
userSubscriptionsSortValidator,
setDefaultSort,
setDefaultPagination,
userSubscriptionListValidator,
asyncMiddleware(getUserSubscriptions)
)
mySubscriptionsRouter.post('/me/subscriptions',
authenticate,
userSubscriptionAddValidator,
addUserSubscription
)
mySubscriptionsRouter.get('/me/subscriptions/:uri',
authenticate,
userSubscriptionGetValidator,
asyncMiddleware(getUserSubscription)
)
mySubscriptionsRouter.delete('/me/subscriptions/:uri',
authenticate,
userSubscriptionGetValidator,
asyncRetryTransactionMiddleware(deleteUserSubscription)
)
// ---------------------------------------------------------------------------
export {
mySubscriptionsRouter
}
// ---------------------------------------------------------------------------
async function areSubscriptionsExist (req: express.Request, res: express.Response) {
const uris = req.query.uris as string[]
const user = res.locals.oauth.token.User
const sanitizedHandles = handlesToNameAndHost(uris)
const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, sanitizedHandles)
const existObject: { [id: string ]: boolean } = {}
for (const sanitizedHandle of sanitizedHandles) {
const obj = results.find(r => {
const server = r.ActorFollowing.Server
return r.ActorFollowing.preferredUsername.toLowerCase() === sanitizedHandle.name.toLowerCase() &&
(
(!server && !sanitizedHandle.host) ||
(server.host === sanitizedHandle.host)
)
})
existObject[sanitizedHandle.handle] = obj !== undefined
}
return res.json(existObject)
}
function addUserSubscription (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const [ name, host ] = req.body.uri.split('@')
const payload = {
name,
host,
assertIsChannel: true,
followerActorId: user.Account.Actor.id
}
JobQueue.Instance.createJobAsync({ type: 'activitypub-follow', payload })
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function getUserSubscription (req: express.Request, res: express.Response) {
const subscription = res.locals.subscription
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(subscription.ActorFollowing.VideoChannel.id)
return res.json(videoChannel.toFormattedJSON())
}
async function deleteUserSubscription (req: express.Request, res: express.Response) {
const subscription = res.locals.subscription
await sequelizeTypescript.transaction(async t => {
if (subscription.state === 'accepted') {
sendUndoFollow(subscription, t)
}
return subscription.destroy({ transaction: t })
})
return res.type('json')
.status(HttpStatusCode.NO_CONTENT_204)
.end()
}
async function getUserSubscriptions (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const actorId = user.Account.Actor.id
const resultList = await ActorFollowModel.listSubscriptionsForApi({
actorId,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function getUserSubscriptionVideos (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const countVideos = getCountVideos(req)
const query = pickCommonVideoQuery(req.query)
const apiOptions = await Hooks.wrapObject({
...query,
displayOnlyForFollower: {
actorId: user.Account.Actor.id,
orLocalVideos: false
},
nsfw: buildNSFWFilter(res, query.nsfw),
user,
countVideos
}, 'filter:api.user.me.subscription-videos.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoModel.listForApi.bind(VideoModel),
apiOptions,
'filter:api.user.me.subscription-videos.list.result'
)
return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
}
+51
ファイルの表示
@@ -0,0 +1,51 @@
import express from 'express'
import { forceNumber } from '@peertube/peertube-core-utils'
import { VideosExistInPlaylists } from '@peertube/peertube-models'
import { uuidToShort } from '@peertube/peertube-node-utils'
import { asyncMiddleware, authenticate } from '../../../middlewares/index.js'
import { doVideosInPlaylistExistValidator } from '../../../middlewares/validators/videos/video-playlists.js'
import { VideoPlaylistModel } from '../../../models/video/video-playlist.js'
const myVideoPlaylistsRouter = express.Router()
myVideoPlaylistsRouter.get('/me/video-playlists/videos-exist',
authenticate,
doVideosInPlaylistExistValidator,
asyncMiddleware(doVideosInPlaylistExist)
)
// ---------------------------------------------------------------------------
export {
myVideoPlaylistsRouter
}
// ---------------------------------------------------------------------------
async function doVideosInPlaylistExist (req: express.Request, res: express.Response) {
const videoIds = req.query.videoIds.map(i => forceNumber(i))
const user = res.locals.oauth.token.User
const results = await VideoPlaylistModel.listPlaylistSummariesOf(user.Account.id, videoIds)
const existObject: VideosExistInPlaylists = {}
for (const videoId of videoIds) {
existObject[videoId] = []
}
for (const result of results) {
for (const element of result.VideoPlaylistElements) {
existObject[element.videoId].push({
playlistElementId: element.id,
playlistId: result.id,
playlistDisplayName: result.name,
playlistShortUUID: uuidToShort(result.uuid),
startTimestamp: element.startTimestamp,
stopTimestamp: element.stopTimestamp
})
}
}
return res.json(existObject)
}
+253
ファイルの表示
@@ -0,0 +1,253 @@
import express from 'express'
import { Emailer } from '@server/lib/emailer.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { UserRegistrationModel } from '@server/models/user/user-registration.js'
import { pick } from '@peertube/peertube-core-utils'
import {
HttpStatusCode,
UserRegister,
UserRegistrationRequest,
UserRegistrationState,
UserRegistrationUpdateState,
UserRight
} from '@peertube/peertube-models'
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger.js'
import { logger } from '../../../helpers/logger.js'
import { CONFIG } from '../../../initializers/config.js'
import { Notifier } from '../../../lib/notifier/index.js'
import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user.js'
import {
acceptOrRejectRegistrationValidator,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
buildRateLimiter,
ensureUserHasRight,
ensureUserRegistrationAllowedFactory,
ensureUserRegistrationAllowedForIP,
getRegistrationValidator,
listRegistrationsValidator,
paginationValidator,
setDefaultPagination,
setDefaultSort,
userRegistrationsSortValidator,
usersDirectRegistrationValidator,
usersRequestRegistrationValidator
} from '../../../middlewares/index.js'
const auditLogger = auditLoggerFactory('users')
const registrationRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
max: CONFIG.RATES_LIMIT.SIGNUP.MAX,
skipFailedRequests: true
})
const registrationsRouter = express.Router()
registrationsRouter.post('/registrations/request',
registrationRateLimiter,
asyncMiddleware(ensureUserRegistrationAllowedFactory('request-registration')),
ensureUserRegistrationAllowedForIP,
asyncMiddleware(usersRequestRegistrationValidator),
asyncRetryTransactionMiddleware(requestRegistration)
)
registrationsRouter.post('/registrations/:registrationId/accept',
authenticate,
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
asyncMiddleware(acceptOrRejectRegistrationValidator),
asyncRetryTransactionMiddleware(acceptRegistration)
)
registrationsRouter.post('/registrations/:registrationId/reject',
authenticate,
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
asyncMiddleware(acceptOrRejectRegistrationValidator),
asyncRetryTransactionMiddleware(rejectRegistration)
)
registrationsRouter.delete('/registrations/:registrationId',
authenticate,
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
asyncMiddleware(getRegistrationValidator),
asyncRetryTransactionMiddleware(deleteRegistration)
)
registrationsRouter.get('/registrations',
authenticate,
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
paginationValidator,
userRegistrationsSortValidator,
setDefaultSort,
setDefaultPagination,
listRegistrationsValidator,
asyncMiddleware(listRegistrations)
)
registrationsRouter.post('/register',
registrationRateLimiter,
asyncMiddleware(ensureUserRegistrationAllowedFactory('direct-registration')),
ensureUserRegistrationAllowedForIP,
asyncMiddleware(usersDirectRegistrationValidator),
asyncRetryTransactionMiddleware(registerUser)
)
// ---------------------------------------------------------------------------
export {
registrationsRouter
}
// ---------------------------------------------------------------------------
async function requestRegistration (req: express.Request, res: express.Response) {
const body: UserRegistrationRequest = req.body
const registration = new UserRegistrationModel({
...pick(body, [ 'username', 'password', 'email', 'registrationReason' ]),
accountDisplayName: body.displayName,
channelDisplayName: body.channel?.displayName,
channelHandle: body.channel?.name,
state: UserRegistrationState.PENDING,
emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
})
await registration.save()
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
await sendVerifyRegistrationEmail(registration)
}
Notifier.Instance.notifyOnNewRegistrationRequest(registration)
Hooks.runAction('action:api.user.requested-registration', { body, registration, req, res })
return res.json(registration.toFormattedJSON())
}
// ---------------------------------------------------------------------------
async function acceptRegistration (req: express.Request, res: express.Response) {
const registration = res.locals.userRegistration
const body: UserRegistrationUpdateState = req.body
const userToCreate = buildUser({
username: registration.username,
password: registration.password,
email: registration.email,
emailVerified: registration.emailVerified
})
// We already encrypted password in registration model
userToCreate.skipPasswordEncryption = true
// TODO: handle conflicts if someone else created a channel handle/user handle/user email between registration and approval
const { user } = await createUserAccountAndChannelAndPlaylist({
userToCreate,
userDisplayName: registration.accountDisplayName,
channelNames: registration.channelHandle && registration.channelDisplayName
? {
name: registration.channelHandle,
displayName: registration.channelDisplayName
}
: undefined
})
registration.userId = user.id
registration.state = UserRegistrationState.ACCEPTED
registration.moderationResponse = body.moderationResponse
if (!registration.processedAt) registration.processedAt = new Date()
await registration.save()
logger.info('Registration of %s accepted', registration.username)
if (body.preventEmailDelivery !== true) {
Emailer.Instance.addUserRegistrationRequestProcessedJob(registration)
}
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function rejectRegistration (req: express.Request, res: express.Response) {
const registration = res.locals.userRegistration
const body: UserRegistrationUpdateState = req.body
registration.state = UserRegistrationState.REJECTED
registration.moderationResponse = body.moderationResponse
if (!registration.processedAt) registration.processedAt = new Date()
await registration.save()
if (body.preventEmailDelivery !== true) {
Emailer.Instance.addUserRegistrationRequestProcessedJob(registration)
}
logger.info('Registration of %s rejected', registration.username)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
// ---------------------------------------------------------------------------
async function deleteRegistration (req: express.Request, res: express.Response) {
const registration = res.locals.userRegistration
await registration.destroy()
logger.info('Registration of %s deleted', registration.username)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
// ---------------------------------------------------------------------------
async function listRegistrations (req: express.Request, res: express.Response) {
const resultList = await UserRegistrationModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search
})
return res.json({
total: resultList.total,
data: resultList.data.map(d => d.toFormattedJSON())
})
}
// ---------------------------------------------------------------------------
async function registerUser (req: express.Request, res: express.Response) {
const body: UserRegister = req.body
const userToCreate = buildUser({
...pick(body, [ 'username', 'password', 'email' ]),
emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
})
const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({
userToCreate,
userDisplayName: body.displayName || undefined,
channelNames: body.channel
})
auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
logger.info('User %s with its channel and account registered.', body.username)
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
await sendVerifyUserEmail(user)
}
Notifier.Instance.notifyOnNewDirectRegistration(user)
Hooks.runAction('action:api.user.registered', { body, user, account, videoChannel, req, res })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+132
ファイルの表示
@@ -0,0 +1,132 @@
import express from 'express'
import { ScopedToken } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { CONFIG } from '@server/initializers/config.js'
import { OTP } from '@server/initializers/constants.js'
import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth.js'
import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model.js'
import { handleOAuthToken, MissingTwoFactorError } from '@server/lib/auth/oauth.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares/index.js'
import { buildUUID } from '@peertube/peertube-node-utils'
const tokensRouter = express.Router()
const loginRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
max: CONFIG.RATES_LIMIT.LOGIN.MAX
})
tokensRouter.post('/token',
loginRateLimiter,
openapiOperationDoc({ operationId: 'getOAuthToken' }),
asyncMiddleware(handleToken)
)
tokensRouter.post('/revoke-token',
openapiOperationDoc({ operationId: 'revokeOAuthToken' }),
authenticate,
asyncMiddleware(handleTokenRevocation)
)
tokensRouter.get('/scoped-tokens',
authenticate,
getScopedTokens
)
tokensRouter.post('/scoped-tokens',
authenticate,
asyncMiddleware(renewScopedTokens)
)
// ---------------------------------------------------------------------------
export {
tokensRouter
}
// ---------------------------------------------------------------------------
async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) {
const grantType = req.body.grant_type
try {
const bypassLogin = await buildByPassLogin(req, grantType)
const refreshTokenAuthName = grantType === 'refresh_token'
? await getAuthNameFromRefreshGrant(req.body.refresh_token)
: undefined
const options = {
refreshTokenAuthName,
bypassLogin
}
const token = await handleOAuthToken(req, options)
res.set('Cache-Control', 'no-store')
res.set('Pragma', 'no-cache')
Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip, req, res })
return res.json({
token_type: 'Bearer',
access_token: token.accessToken,
refresh_token: token.refreshToken,
expires_in: token.accessTokenExpiresIn,
refresh_token_expires_in: token.refreshTokenExpiresIn
})
} catch (err) {
if (err instanceof MissingTwoFactorError) {
res.set(OTP.HEADER_NAME, OTP.HEADER_REQUIRED_VALUE)
logger.debug('Missing two factor error', { err })
} else {
logger.warn('Login error', { err })
}
return res.fail({
status: err.code,
message: err.message,
type: err.name
})
}
}
async function handleTokenRevocation (req: express.Request, res: express.Response) {
const token = res.locals.oauth.token
const result = await revokeToken(token, { req, explicitLogout: true })
return res.json(result)
}
function getScopedTokens (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user
return res.json({
feedToken: user.feedToken
} as ScopedToken)
}
async function renewScopedTokens (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user
user.feedToken = buildUUID()
await user.save()
return res.json({
feedToken: user.feedToken
} as ScopedToken)
}
async function buildByPassLogin (req: express.Request, grantType: string): Promise<BypassLogin> {
if (grantType !== 'password') return undefined
if (req.body.externalAuthToken) {
// Consistency with the getBypassFromPasswordGrant promise
return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
}
return getBypassFromPasswordGrant(req.body.username, req.body.password)
}
+95
ファイルの表示
@@ -0,0 +1,95 @@
import express from 'express'
import { generateOTPSecret, isOTPValid } from '@server/helpers/otp.js'
import { encrypt } from '@server/helpers/peertube-crypto.js'
import { CONFIG } from '@server/initializers/config.js'
import { Redis } from '@server/lib/redis.js'
import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares/index.js'
import {
confirmTwoFactorValidator,
disableTwoFactorValidator,
requestOrConfirmTwoFactorValidator
} from '@server/middlewares/validators/two-factor.js'
import { HttpStatusCode, TwoFactorEnableResult } from '@peertube/peertube-models'
const twoFactorRouter = express.Router()
twoFactorRouter.post('/:id/two-factor/request',
authenticate,
asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
asyncMiddleware(requestOrConfirmTwoFactorValidator),
asyncMiddleware(requestTwoFactor)
)
twoFactorRouter.post('/:id/two-factor/confirm-request',
authenticate,
asyncMiddleware(requestOrConfirmTwoFactorValidator),
confirmTwoFactorValidator,
asyncMiddleware(confirmRequestTwoFactor)
)
twoFactorRouter.post('/:id/two-factor/disable',
authenticate,
asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
asyncMiddleware(disableTwoFactorValidator),
asyncMiddleware(disableTwoFactor)
)
// ---------------------------------------------------------------------------
export {
twoFactorRouter
}
// ---------------------------------------------------------------------------
async function requestTwoFactor (req: express.Request, res: express.Response) {
const user = res.locals.user
const { secret, uri } = generateOTPSecret(user.email)
const encryptedSecret = await encrypt(secret, CONFIG.SECRETS.PEERTUBE)
const requestToken = await Redis.Instance.setTwoFactorRequest(user.id, encryptedSecret)
return res.json({
otpRequest: {
requestToken,
secret,
uri
}
} as TwoFactorEnableResult)
}
async function confirmRequestTwoFactor (req: express.Request, res: express.Response) {
const requestToken = req.body.requestToken
const otpToken = req.body.otpToken
const user = res.locals.user
const encryptedSecret = await Redis.Instance.getTwoFactorRequestToken(user.id, requestToken)
if (!encryptedSecret) {
return res.fail({
message: 'Invalid request token',
status: HttpStatusCode.FORBIDDEN_403
})
}
if (await isOTPValid({ encryptedSecret, token: otpToken }) !== true) {
return res.fail({
message: 'Invalid OTP token',
status: HttpStatusCode.FORBIDDEN_403
})
}
user.otpSecret = encryptedSecret
await user.save()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function disableTwoFactor (req: express.Request, res: express.Response) {
const user = res.locals.user
user.otpSecret = null
await user.save()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+97
ファイルの表示
@@ -0,0 +1,97 @@
import express from 'express'
import { FileStorage, HttpStatusCode, UserExportRequest, UserExportRequestResult, UserExportState } from '@peertube/peertube-models'
import {
asyncMiddleware,
authenticate,
userExportDeleteValidator,
userExportRequestValidator,
userExportsListValidator
} from '../../../middlewares/index.js'
import { UserExportModel } from '@server/models/user/user-export.js'
import { getFormattedObjects } from '@server/helpers/utils.js'
import { sequelizeTypescript } from '@server/initializers/database.js'
import { JobQueue } from '@server/lib/job-queue/job-queue.js'
import { CONFIG } from '@server/initializers/config.js'
const userExportsRouter = express.Router()
userExportsRouter.post('/:userId/exports/request',
authenticate,
asyncMiddleware(userExportRequestValidator),
asyncMiddleware(requestExport)
)
userExportsRouter.get('/:userId/exports',
authenticate,
asyncMiddleware(userExportsListValidator),
asyncMiddleware(listUserExports)
)
userExportsRouter.delete('/:userId/exports/:id',
authenticate,
asyncMiddleware(userExportDeleteValidator),
asyncMiddleware(deleteUserExport)
)
// ---------------------------------------------------------------------------
export {
userExportsRouter
}
// ---------------------------------------------------------------------------
async function requestExport (req: express.Request, res: express.Response) {
const body = req.body as UserExportRequest
const exportModel = new UserExportModel({
state: UserExportState.PENDING,
withVideoFiles: body.withVideoFiles,
storage: CONFIG.OBJECT_STORAGE.ENABLED
? FileStorage.OBJECT_STORAGE
: FileStorage.FILE_SYSTEM,
userId: res.locals.user.id,
createdAt: new Date()
})
exportModel.generateAndSetFilename()
await sequelizeTypescript.transaction(async transaction => {
await exportModel.save({ transaction })
})
await JobQueue.Instance.createJob({ type: 'create-user-export', payload: { userExportId: exportModel.id } })
return res.json({
export: {
id: exportModel.id
}
} as UserExportRequestResult)
}
async function listUserExports (req: express.Request, res: express.Response) {
const resultList = await UserExportModel.listForApi({
start: req.query.start,
count: req.query.count,
user: res.locals.user
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function deleteUserExport (req: express.Request, res: express.Response) {
const userExport = res.locals.userExport
await sequelizeTypescript.transaction(async transaction => {
await userExport.reload({ transaction })
if (!userExport.canBeSafelyRemoved()) {
return res.sendStatus(HttpStatusCode.CONFLICT_409)
}
await userExport.destroy({ transaction })
})
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+79
ファイルの表示
@@ -0,0 +1,79 @@
import express from 'express'
import {
asyncMiddleware,
authenticate
} from '../../../middlewares/index.js'
import { setupUploadResumableRoutes } from '@server/lib/uploadx.js'
import {
getLatestImportStatusValidator,
userImportRequestResumableInitValidator,
userImportRequestResumableValidator
} from '@server/middlewares/validators/users/user-import.js'
import { HttpStatusCode, UserImportState, UserImportUploadResult } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { UserImportModel } from '@server/models/user/user-import.js'
import { getFSUserImportFilePath } from '@server/lib/paths.js'
import { move } from 'fs-extra/esm'
import { JobQueue } from '@server/lib/job-queue/job-queue.js'
import { saveInTransactionWithRetries } from '@server/helpers/database-utils.js'
const userImportRouter = express.Router()
userImportRouter.get('/:userId/imports/latest',
authenticate,
asyncMiddleware(getLatestImportStatusValidator),
asyncMiddleware(getLatestImport)
)
setupUploadResumableRoutes({
routePath: '/:userId/imports/import-resumable',
router: userImportRouter,
uploadInitAfterMiddlewares: [ asyncMiddleware(userImportRequestResumableInitValidator) ],
uploadedMiddlewares: [ asyncMiddleware(userImportRequestResumableValidator) ],
uploadedController: asyncMiddleware(addUserImportResumable)
})
// ---------------------------------------------------------------------------
export {
userImportRouter
}
// ---------------------------------------------------------------------------
async function addUserImportResumable (req: express.Request, res: express.Response) {
const file = res.locals.importUserFileResumable
const user = res.locals.user
// Move import
const userImport = new UserImportModel({
state: UserImportState.PENDING,
userId: user.id,
createdAt: new Date()
})
userImport.generateAndSetFilename()
await move(file.path, getFSUserImportFilePath(userImport))
await saveInTransactionWithRetries(userImport)
// Create job
await JobQueue.Instance.createJob({ type: 'import-user-archive', payload: { userImportId: userImport.id } })
logger.info('User import request job created for user ' + user.username)
return res.json({
userImport: {
id: userImport.id
}
} as UserImportUploadResult)
}
async function getLatestImport (req: express.Request, res: express.Response) {
const userImport = await UserImportModel.loadLatestByUserId(res.locals.user.id)
if (!userImport) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
return res.json(userImport.toFormattedJSON())
}
+79
ファイルの表示
@@ -0,0 +1,79 @@
import express from 'express'
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelSyncAuditView } from '@server/helpers/audit-logger.js'
import { logger } from '@server/helpers/logger.js'
import {
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
ensureCanManageChannelOrAccount,
ensureSyncExists,
ensureSyncIsEnabled,
videoChannelSyncValidator
} from '@server/middlewares/index.js'
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
import { MChannelSyncFormattable } from '@server/types/models/index.js'
import { HttpStatusCode, VideoChannelSyncState } from '@peertube/peertube-models'
const videoChannelSyncRouter = express.Router()
const auditLogger = auditLoggerFactory('channel-syncs')
videoChannelSyncRouter.use(apiRateLimiter)
videoChannelSyncRouter.post('/',
authenticate,
ensureSyncIsEnabled,
asyncMiddleware(videoChannelSyncValidator),
ensureCanManageChannelOrAccount,
asyncRetryTransactionMiddleware(createVideoChannelSync)
)
videoChannelSyncRouter.delete('/:id',
authenticate,
asyncMiddleware(ensureSyncExists),
ensureCanManageChannelOrAccount,
asyncRetryTransactionMiddleware(removeVideoChannelSync)
)
export { videoChannelSyncRouter }
// ---------------------------------------------------------------------------
async function createVideoChannelSync (req: express.Request, res: express.Response) {
const syncCreated: MChannelSyncFormattable = new VideoChannelSyncModel({
externalChannelUrl: req.body.externalChannelUrl,
videoChannelId: req.body.videoChannelId,
state: VideoChannelSyncState.WAITING_FIRST_RUN
})
await syncCreated.save()
syncCreated.VideoChannel = res.locals.videoChannel
auditLogger.create(getAuditIdFromRes(res), new VideoChannelSyncAuditView(syncCreated.toFormattedJSON()))
logger.info(
'Video synchronization for channel "%s" with external channel "%s" created.',
syncCreated.VideoChannel.name,
syncCreated.externalChannelUrl
)
return res.json({
videoChannelSync: syncCreated.toFormattedJSON()
})
}
async function removeVideoChannelSync (req: express.Request, res: express.Response) {
const syncInstance = res.locals.videoChannelSync
await syncInstance.destroy()
auditLogger.delete(getAuditIdFromRes(res), new VideoChannelSyncAuditView(syncInstance.toFormattedJSON()))
logger.info(
'Video synchronization for channel "%s" with external channel "%s" deleted.',
syncInstance.VideoChannel.name,
syncInstance.externalChannelUrl
)
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
+454
ファイルの表示
@@ -0,0 +1,454 @@
import express from 'express'
import {
ActorImageType,
HttpStatusCode,
VideoChannelCreate,
VideoChannelUpdate,
VideosImportInChannelCreate
} from '@peertube/peertube-models'
import { pickCommonVideoQuery } from '@server/helpers/query.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { ActorFollowModel } from '@server/models/actor/actor-follow.js'
import { getServerActor } from '@server/models/application/application.js'
import { MChannelBannerAccountDefault } from '@server/types/models/index.js'
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger.js'
import { resetSequelizeInstance } from '../../helpers/database-utils.js'
import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils.js'
import { logger } from '../../helpers/logger.js'
import { getFormattedObjects } from '../../helpers/utils.js'
import { MIMETYPES } from '../../initializers/constants.js'
import { sequelizeTypescript } from '../../initializers/database.js'
import { sendUpdateActor } from '../../lib/activitypub/send/index.js'
import { JobQueue } from '../../lib/job-queue/index.js'
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor.js'
import { createLocalVideoChannelWithoutKeys, federateAllVideosOfChannel } from '../../lib/video-channel.js'
import {
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
commonVideosFiltersValidator,
ensureCanManageChannelOrAccount,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort,
setDefaultVideosSort,
videoChannelsAddValidator,
videoChannelsRemoveValidator,
videoChannelsSortValidator,
videoChannelsUpdateValidator,
videoPlaylistsSortValidator
} from '../../middlewares/index.js'
import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image.js'
import {
ensureChannelOwnerCanUpload,
ensureIsLocalChannel,
videoChannelImportVideosValidator,
videoChannelsFollowersSortValidator,
videoChannelsListValidator,
videoChannelsNameWithHostValidator,
videosSortValidator
} from '../../middlewares/validators/index.js'
import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists.js'
import { AccountModel } from '../../models/account/account.js'
import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter/index.js'
import { VideoChannelModel } from '../../models/video/video-channel.js'
import { VideoPlaylistModel } from '../../models/video/video-playlist.js'
import { VideoModel } from '../../models/video/video.js'
const auditLogger = auditLoggerFactory('channels')
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
const videoChannelRouter = express.Router()
videoChannelRouter.use(apiRateLimiter)
videoChannelRouter.get('/',
paginationValidator,
videoChannelsSortValidator,
setDefaultSort,
setDefaultPagination,
videoChannelsListValidator,
asyncMiddleware(listVideoChannels)
)
videoChannelRouter.post('/',
authenticate,
asyncMiddleware(videoChannelsAddValidator),
asyncRetryTransactionMiddleware(createVideoChannel)
)
videoChannelRouter.post('/:nameWithHost/avatar/pick',
authenticate,
reqAvatarFile,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
ensureCanManageChannelOrAccount,
updateAvatarValidator,
asyncMiddleware(updateVideoChannelAvatar)
)
videoChannelRouter.post('/:nameWithHost/banner/pick',
authenticate,
reqBannerFile,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
ensureCanManageChannelOrAccount,
updateBannerValidator,
asyncMiddleware(updateVideoChannelBanner)
)
videoChannelRouter.delete('/:nameWithHost/avatar',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
ensureCanManageChannelOrAccount,
asyncMiddleware(deleteVideoChannelAvatar)
)
videoChannelRouter.delete('/:nameWithHost/banner',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
ensureCanManageChannelOrAccount,
asyncMiddleware(deleteVideoChannelBanner)
)
videoChannelRouter.put('/:nameWithHost',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
ensureCanManageChannelOrAccount,
videoChannelsUpdateValidator,
asyncRetryTransactionMiddleware(updateVideoChannel)
)
videoChannelRouter.delete('/:nameWithHost',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
ensureCanManageChannelOrAccount,
asyncMiddleware(videoChannelsRemoveValidator),
asyncRetryTransactionMiddleware(removeVideoChannel)
)
videoChannelRouter.get('/:nameWithHost',
asyncMiddleware(videoChannelsNameWithHostValidator),
asyncMiddleware(getVideoChannel)
)
videoChannelRouter.get('/:nameWithHost/video-playlists',
optionalAuthenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
paginationValidator,
videoPlaylistsSortValidator,
setDefaultSort,
setDefaultPagination,
commonVideoPlaylistFiltersValidator,
asyncMiddleware(listVideoChannelPlaylists)
)
videoChannelRouter.get('/:nameWithHost/videos',
asyncMiddleware(videoChannelsNameWithHostValidator),
paginationValidator,
videosSortValidator,
setDefaultVideosSort,
setDefaultPagination,
optionalAuthenticate,
commonVideosFiltersValidator,
asyncMiddleware(listVideoChannelVideos)
)
videoChannelRouter.get('/:nameWithHost/followers',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureCanManageChannelOrAccount,
paginationValidator,
videoChannelsFollowersSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listVideoChannelFollowers)
)
videoChannelRouter.post('/:nameWithHost/import-videos',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
asyncMiddleware(videoChannelImportVideosValidator),
ensureIsLocalChannel,
ensureCanManageChannelOrAccount,
asyncMiddleware(ensureChannelOwnerCanUpload),
asyncMiddleware(importVideosInChannel)
)
// ---------------------------------------------------------------------------
export {
videoChannelRouter
}
// ---------------------------------------------------------------------------
async function listVideoChannels (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const apiOptions = await Hooks.wrapObject({
actorId: serverActor.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort
}, 'filter:api.video-channels.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoChannelModel.listForApi.bind(VideoChannelModel),
apiOptions,
'filter:api.video-channels.list.result'
)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function updateVideoChannelBanner (req: express.Request, res: express.Response) {
const bannerPhysicalFile = req.files['bannerfile'][0]
const videoChannel = res.locals.videoChannel
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
const banners = await updateLocalActorImageFiles({
accountOrChannel: videoChannel,
imagePhysicalFile: bannerPhysicalFile,
type: ActorImageType.BANNER,
sendActorUpdate: true
})
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
return res.json({
banners: banners.map(b => b.toFormattedJSON())
})
}
async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
const avatarPhysicalFile = req.files['avatarfile'][0]
const videoChannel = res.locals.videoChannel
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
const avatars = await updateLocalActorImageFiles({
accountOrChannel: videoChannel,
imagePhysicalFile: avatarPhysicalFile,
type: ActorImageType.AVATAR,
sendActorUpdate: true
})
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
return res.json({
avatars: avatars.map(a => a.toFormattedJSON())
})
}
async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
const videoChannel = res.locals.videoChannel
await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
const videoChannel = res.locals.videoChannel
await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function createVideoChannel (req: express.Request, res: express.Response) {
const videoChannelInfo: VideoChannelCreate = req.body
const videoChannelCreated = await sequelizeTypescript.transaction(async t => {
const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
return createLocalVideoChannelWithoutKeys(videoChannelInfo, account, t)
})
await JobQueue.Instance.createJob({
type: 'actor-keys',
payload: { actorId: videoChannelCreated.actorId }
})
auditLogger.create(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelCreated.toFormattedJSON()))
logger.info('Video channel %s created.', videoChannelCreated.Actor.url)
Hooks.runAction('action:api.video-channel.created', { videoChannel: videoChannelCreated, req, res })
return res.json({
videoChannel: {
id: videoChannelCreated.id
}
})
}
async function updateVideoChannel (req: express.Request, res: express.Response) {
const videoChannelInstance = res.locals.videoChannel
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())
const videoChannelInfoToUpdate = req.body as VideoChannelUpdate
let doBulkVideoUpdate = false
try {
await sequelizeTypescript.transaction(async t => {
if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName
if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description
if (videoChannelInfoToUpdate.support !== undefined) {
const oldSupportField = videoChannelInstance.support
videoChannelInstance.support = videoChannelInfoToUpdate.support
if (videoChannelInfoToUpdate.bulkVideosSupportUpdate === true && oldSupportField !== videoChannelInfoToUpdate.support) {
doBulkVideoUpdate = true
await VideoModel.bulkUpdateSupportField(videoChannelInstance, t)
}
}
const videoChannelInstanceUpdated = await videoChannelInstance.save({ transaction: t }) as MChannelBannerAccountDefault
await sendUpdateActor(videoChannelInstanceUpdated, t)
auditLogger.update(
getAuditIdFromRes(res),
new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()),
oldVideoChannelAuditKeys
)
Hooks.runAction('action:api.video-channel.updated', { videoChannel: videoChannelInstanceUpdated, req, res })
logger.info('Video channel %s updated.', videoChannelInstance.Actor.url)
})
} catch (err) {
logger.debug('Cannot update the video channel.', { err })
// If the transaction is retried, sequelize will think the object has not changed
// So we need to restore the previous fields
await resetSequelizeInstance(videoChannelInstance)
throw err
}
res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
// Don't process in a transaction, and after the response because it could be long
if (doBulkVideoUpdate) {
await federateAllVideosOfChannel(videoChannelInstance)
}
}
async function removeVideoChannel (req: express.Request, res: express.Response) {
const videoChannelInstance = res.locals.videoChannel
await sequelizeTypescript.transaction(async t => {
await VideoPlaylistModel.resetPlaylistsOfChannel(videoChannelInstance.id, t)
await videoChannelInstance.destroy({ transaction: t })
Hooks.runAction('action:api.video-channel.deleted', { videoChannel: videoChannelInstance, req, res })
auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()))
logger.info('Video channel %s deleted.', videoChannelInstance.Actor.url)
})
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
async function getVideoChannel (req: express.Request, res: express.Response) {
const id = res.locals.videoChannel.id
const videoChannel = await Hooks.wrapObject(res.locals.videoChannel, 'filter:api.video-channel.get.result', { id })
if (videoChannel.isOutdated()) {
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } })
}
return res.json(videoChannel.toFormattedJSON())
}
async function listVideoChannelPlaylists (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const resultList = await VideoPlaylistModel.listForApi({
followerActorId: isUserAbleToSearchRemoteURI(res)
? null
: serverActor.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
videoChannelId: res.locals.videoChannel.id,
type: req.query.playlistType
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function listVideoChannelVideos (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const videoChannelInstance = res.locals.videoChannel
const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res)
? null
: {
actorId: serverActor.id,
orLocalVideos: true
}
const countVideos = getCountVideos(req)
const query = pickCommonVideoQuery(req.query)
const apiOptions = await Hooks.wrapObject({
...query,
displayOnlyForFollower,
nsfw: buildNSFWFilter(res, query.nsfw),
videoChannelId: videoChannelInstance.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos
}, 'filter:api.video-channels.videos.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoModel.listForApi.bind(VideoModel),
apiOptions,
'filter:api.video-channels.videos.list.result'
)
return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
}
async function listVideoChannelFollowers (req: express.Request, res: express.Response) {
const channel = res.locals.videoChannel
const resultList = await ActorFollowModel.listFollowersForApi({
actorIds: [ channel.actorId ],
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
state: 'accepted'
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function importVideosInChannel (req: express.Request, res: express.Response) {
const { externalChannelUrl } = req.body as VideosImportInChannelCreate
await JobQueue.Instance.createJob({
type: 'video-channel-import',
payload: {
externalChannelUrl,
videoChannelId: res.locals.videoChannel.id,
partOfChannelSyncId: res.locals.videoChannelSync?.id
}
})
logger.info('Video import job for channel "%s" with url "%s" created.', res.locals.videoChannel.name, externalChannelUrl)
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
+490
ファイルの表示
@@ -0,0 +1,490 @@
import { forceNumber } from '@peertube/peertube-core-utils'
import {
HttpStatusCode,
VideoPlaylistCreate,
VideoPlaylistCreateResult,
VideoPlaylistElementCreate,
VideoPlaylistElementCreateResult,
VideoPlaylistElementUpdate,
VideoPlaylistPrivacy,
VideoPlaylistPrivacyType,
VideoPlaylistReorder,
VideoPlaylistUpdate
} from '@peertube/peertube-models'
import { uuidToShort } from '@peertube/peertube-node-utils'
import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists/index.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { generateThumbnailForPlaylist } from '@server/lib/video-playlist.js'
import { getServerActor } from '@server/models/application/application.js'
import { MVideoPlaylistFull, MVideoPlaylistThumbnail } from '@server/types/models/index.js'
import express from 'express'
import { resetSequelizeInstance } from '../../helpers/database-utils.js'
import { createReqFiles } from '../../helpers/express-utils.js'
import { logger } from '../../helpers/logger.js'
import { getFormattedObjects } from '../../helpers/utils.js'
import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants.js'
import { sequelizeTypescript } from '../../initializers/database.js'
import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send/index.js'
import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url.js'
import { updateLocalPlaylistMiniatureFromExisting } from '../../lib/thumbnail.js'
import {
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort
} from '../../middlewares/index.js'
import { videoPlaylistsSortValidator } from '../../middlewares/validators/index.js'
import {
commonVideoPlaylistFiltersValidator,
videoPlaylistsAddValidator,
videoPlaylistsAddVideoValidator,
videoPlaylistsDeleteValidator,
videoPlaylistsGetValidator,
videoPlaylistsReorderVideosValidator,
videoPlaylistsUpdateOrRemoveVideoValidator,
videoPlaylistsUpdateValidator
} from '../../middlewares/validators/videos/video-playlists.js'
import { AccountModel } from '../../models/account/account.js'
import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element.js'
import { VideoPlaylistModel } from '../../models/video/video-playlist.js'
const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
const videoPlaylistRouter = express.Router()
videoPlaylistRouter.use(apiRateLimiter)
videoPlaylistRouter.get('/privacies', listVideoPlaylistPrivacies)
videoPlaylistRouter.get('/',
paginationValidator,
videoPlaylistsSortValidator,
setDefaultSort,
setDefaultPagination,
commonVideoPlaylistFiltersValidator,
asyncMiddleware(listVideoPlaylists)
)
videoPlaylistRouter.get('/:playlistId',
asyncMiddleware(videoPlaylistsGetValidator('summary')),
getVideoPlaylist
)
videoPlaylistRouter.post('/',
authenticate,
reqThumbnailFile,
asyncMiddleware(videoPlaylistsAddValidator),
asyncRetryTransactionMiddleware(createVideoPlaylist)
)
videoPlaylistRouter.put('/:playlistId',
authenticate,
reqThumbnailFile,
asyncMiddleware(videoPlaylistsUpdateValidator),
asyncRetryTransactionMiddleware(updateVideoPlaylist)
)
videoPlaylistRouter.delete('/:playlistId',
authenticate,
asyncMiddleware(videoPlaylistsDeleteValidator),
asyncRetryTransactionMiddleware(removeVideoPlaylist)
)
videoPlaylistRouter.get('/:playlistId/videos',
asyncMiddleware(videoPlaylistsGetValidator('summary')),
paginationValidator,
setDefaultPagination,
optionalAuthenticate,
asyncMiddleware(getVideoPlaylistVideos)
)
videoPlaylistRouter.post('/:playlistId/videos',
authenticate,
asyncMiddleware(videoPlaylistsAddVideoValidator),
asyncRetryTransactionMiddleware(addVideoInPlaylist)
)
videoPlaylistRouter.post('/:playlistId/videos/reorder',
authenticate,
asyncMiddleware(videoPlaylistsReorderVideosValidator),
asyncRetryTransactionMiddleware(reorderVideosPlaylist)
)
videoPlaylistRouter.put('/:playlistId/videos/:playlistElementId',
authenticate,
asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator),
asyncRetryTransactionMiddleware(updateVideoPlaylistElement)
)
videoPlaylistRouter.delete('/:playlistId/videos/:playlistElementId',
authenticate,
asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator),
asyncRetryTransactionMiddleware(removeVideoFromPlaylist)
)
// ---------------------------------------------------------------------------
export {
videoPlaylistRouter
}
// ---------------------------------------------------------------------------
function listVideoPlaylistPrivacies (req: express.Request, res: express.Response) {
res.json(VIDEO_PLAYLIST_PRIVACIES)
}
async function listVideoPlaylists (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const resultList = await VideoPlaylistModel.listForApi({
followerActorId: serverActor.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
type: req.query.playlistType
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
function getVideoPlaylist (req: express.Request, res: express.Response) {
const videoPlaylist = res.locals.videoPlaylistSummary
scheduleRefreshIfNeeded(videoPlaylist)
return res.json(videoPlaylist.toFormattedJSON())
}
async function createVideoPlaylist (req: express.Request, res: express.Response) {
const videoPlaylistInfo: VideoPlaylistCreate = req.body
const user = res.locals.oauth.token.User
const videoPlaylist = new VideoPlaylistModel({
name: videoPlaylistInfo.displayName,
description: videoPlaylistInfo.description,
privacy: videoPlaylistInfo.privacy || VideoPlaylistPrivacy.PRIVATE,
ownerAccountId: user.Account.id
}) as MVideoPlaylistFull
videoPlaylist.url = getLocalVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object
if (videoPlaylistInfo.videoChannelId) {
const videoChannel = res.locals.videoChannel
videoPlaylist.videoChannelId = videoChannel.id
videoPlaylist.VideoChannel = videoChannel
}
const thumbnailField = req.files['thumbnailfile']
const thumbnailModel = thumbnailField
? await updateLocalPlaylistMiniatureFromExisting({
inputPath: thumbnailField[0].path,
playlist: videoPlaylist,
automaticallyGenerated: false
})
: undefined
const videoPlaylistCreated = await sequelizeTypescript.transaction(async t => {
const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) as MVideoPlaylistFull
if (thumbnailModel) {
await videoPlaylistCreated.setAndSaveThumbnail(thumbnailModel, t)
}
// We need more attributes for the federation
videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t)
await sendCreateVideoPlaylist(videoPlaylistCreated, t)
return videoPlaylistCreated
})
logger.info('Video playlist with uuid %s created.', videoPlaylist.uuid)
return res.json({
videoPlaylist: {
id: videoPlaylistCreated.id,
shortUUID: uuidToShort(videoPlaylistCreated.uuid),
uuid: videoPlaylistCreated.uuid
} as VideoPlaylistCreateResult
})
}
async function updateVideoPlaylist (req: express.Request, res: express.Response) {
const videoPlaylistInstance = res.locals.videoPlaylistFull
const videoPlaylistInfoToUpdate = req.body as VideoPlaylistUpdate
const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE
const wasNotPrivatePlaylist = videoPlaylistInstance.privacy !== VideoPlaylistPrivacy.PRIVATE
const thumbnailField = req.files['thumbnailfile']
const thumbnailModel = thumbnailField
? await updateLocalPlaylistMiniatureFromExisting({
inputPath: thumbnailField[0].path,
playlist: videoPlaylistInstance,
automaticallyGenerated: false
})
: undefined
try {
await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = {
transaction: t
}
if (videoPlaylistInfoToUpdate.videoChannelId !== undefined) {
if (videoPlaylistInfoToUpdate.videoChannelId === null) {
videoPlaylistInstance.videoChannelId = null
} else {
const videoChannel = res.locals.videoChannel
videoPlaylistInstance.videoChannelId = videoChannel.id
videoPlaylistInstance.VideoChannel = videoChannel
}
}
if (videoPlaylistInfoToUpdate.displayName !== undefined) videoPlaylistInstance.name = videoPlaylistInfoToUpdate.displayName
if (videoPlaylistInfoToUpdate.description !== undefined) videoPlaylistInstance.description = videoPlaylistInfoToUpdate.description
if (videoPlaylistInfoToUpdate.privacy !== undefined) {
videoPlaylistInstance.privacy = forceNumber(videoPlaylistInfoToUpdate.privacy) as VideoPlaylistPrivacyType
if (wasNotPrivatePlaylist === true && videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE) {
await sendDeleteVideoPlaylist(videoPlaylistInstance, t)
}
}
const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions)
if (thumbnailModel) {
thumbnailModel.automaticallyGenerated = false
await playlistUpdated.setAndSaveThumbnail(thumbnailModel, t)
}
const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE
if (isNewPlaylist) {
await sendCreateVideoPlaylist(playlistUpdated, t)
} else {
await sendUpdateVideoPlaylist(playlistUpdated, t)
}
logger.info('Video playlist %s updated.', videoPlaylistInstance.uuid)
return playlistUpdated
})
} catch (err) {
logger.debug('Cannot update the video playlist.', { err })
// If the transaction is retried, sequelize will think the object has not changed
// So we need to restore the previous fields
await resetSequelizeInstance(videoPlaylistInstance)
throw err
}
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
async function removeVideoPlaylist (req: express.Request, res: express.Response) {
const videoPlaylistInstance = res.locals.videoPlaylistSummary
await sequelizeTypescript.transaction(async t => {
await videoPlaylistInstance.destroy({ transaction: t })
await sendDeleteVideoPlaylist(videoPlaylistInstance, t)
logger.info('Video playlist %s deleted.', videoPlaylistInstance.uuid)
})
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
async function addVideoInPlaylist (req: express.Request, res: express.Response) {
const body: VideoPlaylistElementCreate = req.body
const videoPlaylist = res.locals.videoPlaylistFull
const video = res.locals.onlyVideo
const playlistElement = await sequelizeTypescript.transaction(async t => {
const position = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id, t)
const playlistElement = await VideoPlaylistElementModel.create({
position,
startTimestamp: body.startTimestamp || null,
stopTimestamp: body.stopTimestamp || null,
videoPlaylistId: videoPlaylist.id,
videoId: video.id
}, { transaction: t })
playlistElement.url = getLocalVideoPlaylistElementActivityPubUrl(videoPlaylist, playlistElement)
await playlistElement.save({ transaction: t })
videoPlaylist.changed('updatedAt', true)
await videoPlaylist.save({ transaction: t })
return playlistElement
})
// If the user did not set a thumbnail, automatically take the video thumbnail
if (videoPlaylist.shouldGenerateThumbnailWithNewElement(playlistElement)) {
await generateThumbnailForPlaylist(videoPlaylist, video)
}
sendUpdateVideoPlaylist(videoPlaylist, undefined)
.catch(err => logger.error('Cannot send video playlist update.', { err }))
logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position)
Hooks.runAction('action:api.video-playlist-element.created', { playlistElement, req, res })
return res.json({
videoPlaylistElement: {
id: playlistElement.id
} as VideoPlaylistElementCreateResult
})
}
async function updateVideoPlaylistElement (req: express.Request, res: express.Response) {
const body: VideoPlaylistElementUpdate = req.body
const videoPlaylist = res.locals.videoPlaylistFull
const videoPlaylistElement = res.locals.videoPlaylistElement
const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => {
if (body.startTimestamp !== undefined) videoPlaylistElement.startTimestamp = body.startTimestamp
if (body.stopTimestamp !== undefined) videoPlaylistElement.stopTimestamp = body.stopTimestamp
const element = await videoPlaylistElement.save({ transaction: t })
videoPlaylist.changed('updatedAt', true)
await videoPlaylist.save({ transaction: t })
await sendUpdateVideoPlaylist(videoPlaylist, t)
return element
})
logger.info('Element of position %d of playlist %s updated.', playlistElement.position, videoPlaylist.uuid)
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
async function removeVideoFromPlaylist (req: express.Request, res: express.Response) {
const videoPlaylistElement = res.locals.videoPlaylistElement
const videoPlaylist = res.locals.videoPlaylistFull
const positionToDelete = videoPlaylistElement.position
await sequelizeTypescript.transaction(async t => {
await videoPlaylistElement.destroy({ transaction: t })
// Decrease position of the next elements
await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, positionToDelete, -1, t)
videoPlaylist.changed('updatedAt', true)
await videoPlaylist.save({ transaction: t })
logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid)
})
// Do we need to regenerate the default thumbnail?
if (positionToDelete === 1 && videoPlaylist.hasGeneratedThumbnail()) {
await regeneratePlaylistThumbnail(videoPlaylist)
}
sendUpdateVideoPlaylist(videoPlaylist, undefined)
.catch(err => logger.error('Cannot send video playlist update.', { err }))
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
async function reorderVideosPlaylist (req: express.Request, res: express.Response) {
const videoPlaylist = res.locals.videoPlaylistFull
const body: VideoPlaylistReorder = req.body
const start: number = body.startPosition
const insertAfter: number = body.insertAfterPosition
const reorderLength: number = body.reorderLength || 1
if (start === insertAfter) {
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
// Example: if we reorder position 2 and insert after position 5 (so at position 6): # 1 2 3 4 5 6 7 8 9
// * increase position when position > 5 # 1 2 3 4 5 7 8 9 10
// * update position 2 -> position 6 # 1 3 4 5 6 7 8 9 10
// * decrease position when position position > 2 # 1 2 3 4 5 6 7 8 9
await sequelizeTypescript.transaction(async t => {
const newPosition = insertAfter + 1
// Add space after the position when we want to insert our reordered elements (increase)
await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, newPosition, reorderLength, t)
let oldPosition = start
// We incremented the position of the elements we want to reorder
if (start >= newPosition) oldPosition += reorderLength
const endOldPosition = oldPosition + reorderLength - 1
// Insert our reordered elements in their place (update)
await VideoPlaylistElementModel.reassignPositionOf({
videoPlaylistId: videoPlaylist.id,
firstPosition: oldPosition,
endPosition: endOldPosition,
newPosition,
transaction: t
})
// Decrease positions of elements after the old position of our ordered elements (decrease)
await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, oldPosition, -reorderLength, t)
videoPlaylist.changed('updatedAt', true)
await videoPlaylist.save({ transaction: t })
await sendUpdateVideoPlaylist(videoPlaylist, t)
})
// The first element changed
if ((start === 1 || insertAfter === 0) && videoPlaylist.hasGeneratedThumbnail()) {
await regeneratePlaylistThumbnail(videoPlaylist)
}
logger.info(
'Reordered playlist %s (inserted after position %d elements %d - %d).',
videoPlaylist.uuid, insertAfter, start, start + reorderLength - 1
)
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
async function getVideoPlaylistVideos (req: express.Request, res: express.Response) {
const videoPlaylistInstance = res.locals.videoPlaylistSummary
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
const server = await getServerActor()
const apiOptions = await Hooks.wrapObject({
start: req.query.start,
count: req.query.count,
videoPlaylistId: videoPlaylistInstance.id,
serverAccount: server.Account,
user
}, 'filter:api.video-playlist.videos.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoPlaylistElementModel.listForApi.bind(VideoPlaylistElementModel),
apiOptions,
'filter:api.video-playlist.videos.list.result'
)
const options = { accountId: user?.Account?.id }
return res.json(getFormattedObjects(resultList.data, resultList.total, options))
}
async function regeneratePlaylistThumbnail (videoPlaylist: MVideoPlaylistThumbnail) {
await videoPlaylist.Thumbnail.destroy()
videoPlaylist.Thumbnail = null
const firstElement = await VideoPlaylistElementModel.loadFirstElementWithVideoThumbnail(videoPlaylist.id)
if (firstElement) await generateThumbnailForPlaylist(videoPlaylist, firstElement.Video)
}
+112
ファイルの表示
@@ -0,0 +1,112 @@
import express from 'express'
import { blacklistVideo, unblacklistVideo } from '@server/lib/video-blacklist.js'
import { HttpStatusCode, UserRight, VideoBlacklistCreate } from '@peertube/peertube-models'
import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import {
asyncMiddleware,
authenticate,
blacklistSortValidator,
ensureUserHasRight,
openapiOperationDoc,
paginationValidator,
setBlacklistSort,
setDefaultPagination,
videosBlacklistAddValidator,
videosBlacklistFiltersValidator,
videosBlacklistRemoveValidator,
videosBlacklistUpdateValidator
} from '../../../middlewares/index.js'
import { VideoBlacklistModel } from '../../../models/video/video-blacklist.js'
const blacklistRouter = express.Router()
blacklistRouter.post('/:videoId/blacklist',
openapiOperationDoc({ operationId: 'addVideoBlock' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
asyncMiddleware(videosBlacklistAddValidator),
asyncMiddleware(addVideoToBlacklistController)
)
blacklistRouter.get('/blacklist',
openapiOperationDoc({ operationId: 'getVideoBlocks' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
paginationValidator,
blacklistSortValidator,
setBlacklistSort,
setDefaultPagination,
videosBlacklistFiltersValidator,
asyncMiddleware(listBlacklist)
)
blacklistRouter.put('/:videoId/blacklist',
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
asyncMiddleware(videosBlacklistUpdateValidator),
asyncMiddleware(updateVideoBlacklistController)
)
blacklistRouter.delete('/:videoId/blacklist',
openapiOperationDoc({ operationId: 'delVideoBlock' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
asyncMiddleware(videosBlacklistRemoveValidator),
asyncMiddleware(removeVideoFromBlacklistController)
)
// ---------------------------------------------------------------------------
export {
blacklistRouter
}
// ---------------------------------------------------------------------------
async function addVideoToBlacklistController (req: express.Request, res: express.Response) {
const videoInstance = res.locals.videoAll
const body: VideoBlacklistCreate = req.body
await blacklistVideo(videoInstance, body)
logger.info('Video %s blacklisted.', videoInstance.uuid)
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
async function updateVideoBlacklistController (req: express.Request, res: express.Response) {
const videoBlacklist = res.locals.videoBlacklist
if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason
await sequelizeTypescript.transaction(t => {
return videoBlacklist.save({ transaction: t })
})
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
async function listBlacklist (req: express.Request, res: express.Response) {
const resultList = await VideoBlacklistModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
type: req.query.type
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function removeVideoFromBlacklistController (req: express.Request, res: express.Response) {
const videoBlacklist = res.locals.videoBlacklist
const video = res.locals.videoAll
await unblacklistVideo(videoBlacklist, video)
logger.info('Video %s removed from blacklist.', video.uuid)
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
+116
ファイルの表示
@@ -0,0 +1,116 @@
import { HttpStatusCode, VideoCaptionGenerate } from '@peertube/peertube-models'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { createLocalCaption, createTranscriptionTaskIfNeeded } from '@server/lib/video-captions.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import express from 'express'
import { createReqFiles } from '../../../helpers/express-utils.js'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { MIMETYPES } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { federateVideoIfNeeded } from '../../../lib/activitypub/videos/index.js'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares/index.js'
import {
addVideoCaptionValidator,
deleteVideoCaptionValidator,
generateVideoCaptionValidator,
listVideoCaptionsValidator
} from '../../../middlewares/validators/index.js'
import { VideoCaptionModel } from '../../../models/video/video-caption.js'
const lTags = loggerTagsFactory('api', 'video-caption')
const reqVideoCaptionAdd = createReqFiles([ 'captionfile' ], MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
const videoCaptionsRouter = express.Router()
videoCaptionsRouter.post('/:videoId/captions/generate',
authenticate,
asyncMiddleware(generateVideoCaptionValidator),
asyncMiddleware(createGenerateVideoCaption)
)
videoCaptionsRouter.get('/:videoId/captions',
asyncMiddleware(listVideoCaptionsValidator),
asyncMiddleware(listVideoCaptions)
)
videoCaptionsRouter.put('/:videoId/captions/:captionLanguage',
authenticate,
reqVideoCaptionAdd,
asyncMiddleware(addVideoCaptionValidator),
asyncRetryTransactionMiddleware(createVideoCaption)
)
videoCaptionsRouter.delete('/:videoId/captions/:captionLanguage',
authenticate,
asyncMiddleware(deleteVideoCaptionValidator),
asyncRetryTransactionMiddleware(deleteVideoCaption)
)
// ---------------------------------------------------------------------------
export {
videoCaptionsRouter
}
// ---------------------------------------------------------------------------
async function createGenerateVideoCaption (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const body = req.body as VideoCaptionGenerate
if (body.forceTranscription === true) {
await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingTranscription')
}
await createTranscriptionTaskIfNeeded(video)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function listVideoCaptions (req: express.Request, res: express.Response) {
const data = await VideoCaptionModel.listVideoCaptions(res.locals.onlyVideo.id)
return res.json(getFormattedObjects(data, data.length))
}
async function createVideoCaption (req: express.Request, res: express.Response) {
const videoCaptionPhysicalFile: Express.Multer.File = req.files['captionfile'][0]
const video = res.locals.videoAll
const captionLanguage = req.params.captionLanguage
const videoCaption = await createLocalCaption({
video,
language: captionLanguage,
path: videoCaptionPhysicalFile.path,
automaticallyGenerated: false
})
await sequelizeTypescript.transaction(async t => {
await federateVideoIfNeeded(video, false, t)
})
Hooks.runAction('action:api.video-caption.created', { caption: videoCaption, req, res })
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function deleteVideoCaption (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const videoCaption = res.locals.videoCaption
await sequelizeTypescript.transaction(async t => {
await videoCaption.destroy({ transaction: t })
// Send video update
await federateVideoIfNeeded(video, false, t)
})
logger.info('Video caption %s of video %s deleted.', videoCaption.language, video.uuid, lTags(video.uuid))
Hooks.runAction('action:api.video-caption.deleted', { caption: videoCaption, req, res })
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
}
+51
ファイルの表示
@@ -0,0 +1,51 @@
import express from 'express'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares/index.js'
import { updateVideoChaptersValidator, videosCustomGetValidator } from '../../../middlewares/validators/index.js'
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
import { HttpStatusCode, VideoChapterUpdate } from '@peertube/peertube-models'
import { sequelizeTypescript } from '@server/initializers/database.js'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/federate.js'
import { replaceChapters } from '@server/lib/video-chapters.js'
const videoChaptersRouter = express.Router()
videoChaptersRouter.get('/:id/chapters',
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(listVideoChapters)
)
videoChaptersRouter.put('/:videoId/chapters',
authenticate,
asyncMiddleware(updateVideoChaptersValidator),
asyncRetryTransactionMiddleware(replaceVideoChapters)
)
// ---------------------------------------------------------------------------
export {
videoChaptersRouter
}
// ---------------------------------------------------------------------------
async function listVideoChapters (req: express.Request, res: express.Response) {
const chapters = await VideoChapterModel.listChaptersOfVideo(res.locals.onlyVideo.id)
return res.json({ chapters: chapters.map(c => c.toFormattedJSON()) })
}
async function replaceVideoChapters (req: express.Request, res: express.Response) {
const body = req.body as VideoChapterUpdate
const video = res.locals.videoAll
await retryTransactionWrapper(() => {
return sequelizeTypescript.transaction(async t => {
await replaceChapters({ video, chapters: body.chapters, transaction: t })
await federateVideoIfNeeded(video, false, t)
})
})
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+248
ファイルの表示
@@ -0,0 +1,248 @@
import { pick } from '@peertube/peertube-core-utils'
import {
HttpStatusCode,
ResultList,
ThreadsResultList,
UserRight,
VideoCommentCreate,
VideoCommentPolicy,
VideoCommentThreads
} from '@peertube/peertube-models'
import { getServerActor } from '@server/models/application/application.js'
import { MCommentFormattable } from '@server/types/models/index.js'
import express from 'express'
import { CommentAuditView, auditLoggerFactory, getAuditIdFromRes } from '../../../helpers/audit-logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { Notifier } from '../../../lib/notifier/index.js'
import { Hooks } from '../../../lib/plugins/hooks.js'
import { approveComment, buildFormattedCommentTree, createLocalVideoComment, removeComment } from '../../../lib/video-comment.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
ensureUserHasRight,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort
} from '../../../middlewares/index.js'
import {
addVideoCommentReplyValidator,
addVideoCommentThreadValidator,
approveVideoCommentValidator,
listAllVideoCommentsForAdminValidator,
listVideoCommentThreadsValidator,
listVideoThreadCommentsValidator,
removeVideoCommentValidator,
videoCommentThreadsSortValidator,
videoCommentsValidator
} from '../../../middlewares/validators/index.js'
import { VideoCommentModel } from '../../../models/video/video-comment.js'
const auditLogger = auditLoggerFactory('comments')
const videoCommentRouter = express.Router()
videoCommentRouter.get('/:videoId/comment-threads',
paginationValidator,
videoCommentThreadsSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listVideoCommentThreadsValidator),
optionalAuthenticate,
asyncMiddleware(listVideoThreads)
)
videoCommentRouter.get('/:videoId/comment-threads/:threadId',
asyncMiddleware(listVideoThreadCommentsValidator),
optionalAuthenticate,
asyncMiddleware(listVideoThreadComments)
)
videoCommentRouter.post('/:videoId/comment-threads',
authenticate,
asyncMiddleware(addVideoCommentThreadValidator),
asyncRetryTransactionMiddleware(addVideoCommentThread)
)
videoCommentRouter.post('/:videoId/comments/:commentId',
authenticate,
asyncMiddleware(addVideoCommentReplyValidator),
asyncRetryTransactionMiddleware(addVideoCommentReply)
)
videoCommentRouter.delete('/:videoId/comments/:commentId',
authenticate,
asyncMiddleware(removeVideoCommentValidator),
asyncRetryTransactionMiddleware(removeVideoComment)
)
videoCommentRouter.post('/:videoId/comments/:commentId/approve',
authenticate,
asyncMiddleware(approveVideoCommentValidator),
asyncMiddleware(approveVideoComment)
)
videoCommentRouter.get('/comments',
authenticate,
ensureUserHasRight(UserRight.SEE_ALL_COMMENTS),
paginationValidator,
videoCommentsValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listAllVideoCommentsForAdminValidator),
asyncMiddleware(listComments)
)
// ---------------------------------------------------------------------------
export {
videoCommentRouter
}
// ---------------------------------------------------------------------------
async function listComments (req: express.Request, res: express.Response) {
const options = {
...pick(req.query, [
'start',
'count',
'sort',
'isLocal',
'onLocalVideo',
'search',
'searchAccount',
'searchVideo',
'autoTagOneOf'
]),
videoId: res.locals.onlyImmutableVideo?.id,
videoChannelOwnerId: res.locals.videoChannel?.id,
autoTagOfAccountId: (await getServerActor()).Account.id,
heldForReview: undefined
}
const resultList = await VideoCommentModel.listCommentsForApi(options)
return res.json({
total: resultList.total,
data: resultList.data.map(c => c.toFormattedForAdminOrUserJSON())
})
}
async function listVideoThreads (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
let resultList: ThreadsResultList<MCommentFormattable>
if (video.commentsPolicy !== VideoCommentPolicy.DISABLED) {
const apiOptions = await Hooks.wrapObject({
video,
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
user
}, 'filter:api.video-threads.list.params')
resultList = await Hooks.wrapPromiseFun(
VideoCommentModel.listThreadsForApi.bind(VideoCommentModel),
apiOptions,
'filter:api.video-threads.list.result'
)
} else {
resultList = {
total: 0,
totalNotDeletedComments: 0,
data: []
}
}
return res.json({
...getFormattedObjects(resultList.data, resultList.total),
totalNotDeletedComments: resultList.totalNotDeletedComments
} as VideoCommentThreads)
}
async function listVideoThreadComments (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
let resultList: ResultList<MCommentFormattable>
if (video.commentsPolicy !== VideoCommentPolicy.DISABLED) {
const apiOptions = await Hooks.wrapObject({
video,
threadId: res.locals.videoCommentThread.id,
user
}, 'filter:api.video-thread-comments.list.params')
resultList = await Hooks.wrapPromiseFun(
VideoCommentModel.listThreadCommentsForApi.bind(VideoCommentModel),
apiOptions,
'filter:api.video-thread-comments.list.result'
)
} else {
resultList = {
total: 0,
data: []
}
}
if (resultList.data.length === 0) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'No comments were found'
})
}
return res.json(buildFormattedCommentTree(resultList))
}
async function addVideoCommentThread (req: express.Request, res: express.Response) {
const videoCommentInfo: VideoCommentCreate = req.body
const comment = await createLocalVideoComment({
text: videoCommentInfo.text,
inReplyToComment: null,
video: res.locals.videoAll,
user: res.locals.oauth.token.User
})
Notifier.Instance.notifyOnNewComment(comment)
auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
Hooks.runAction('action:api.video-thread.created', { comment, req, res })
return res.json({ comment: comment.toFormattedJSON() })
}
async function addVideoCommentReply (req: express.Request, res: express.Response) {
const videoCommentInfo: VideoCommentCreate = req.body
const comment = await createLocalVideoComment({
text: videoCommentInfo.text,
inReplyToComment: res.locals.videoCommentFull,
video: res.locals.videoAll,
user: res.locals.oauth.token.User
})
Notifier.Instance.notifyOnNewComment(comment)
auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
Hooks.runAction('action:api.video-comment-reply.created', { comment, req, res })
return res.json({ comment: comment.toFormattedJSON() })
}
async function removeVideoComment (req: express.Request, res: express.Response) {
const comment = res.locals.videoCommentFull
await removeComment(comment, req, res)
auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function approveVideoComment (req: express.Request, res: express.Response) {
await approveComment(res.locals.videoCommentFull)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+122
ファイルの表示
@@ -0,0 +1,122 @@
import express from 'express'
import validator from 'validator'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
import { updatePlaylistAfterFileChange } from '@server/lib/hls.js'
import { removeAllWebVideoFiles, removeHLSFile, removeHLSPlaylist, removeWebVideoFile } from '@server/lib/video-file.js'
import { VideoFileModel } from '@server/models/video/video-file.js'
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
import {
asyncMiddleware,
authenticate,
ensureUserHasRight,
videoFileMetadataGetValidator,
videoFilesDeleteHLSFileValidator,
videoFilesDeleteHLSValidator,
videoFilesDeleteWebVideoFileValidator,
videoFilesDeleteWebVideoValidator,
videosGetValidator
} from '../../../middlewares/index.js'
const lTags = loggerTagsFactory('api', 'video')
const filesRouter = express.Router()
filesRouter.get('/:id/metadata/:videoFileId',
asyncMiddleware(videosGetValidator),
asyncMiddleware(videoFileMetadataGetValidator),
asyncMiddleware(getVideoFileMetadata)
)
filesRouter.delete('/:id/hls',
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
asyncMiddleware(videoFilesDeleteHLSValidator),
asyncMiddleware(removeHLSPlaylistController)
)
filesRouter.delete('/:id/hls/:videoFileId',
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
asyncMiddleware(videoFilesDeleteHLSFileValidator),
asyncMiddleware(removeHLSFileController)
)
filesRouter.delete(
[ '/:id/webtorrent', '/:id/web-videos' ], // TODO: remove webtorrent in V7
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
asyncMiddleware(videoFilesDeleteWebVideoValidator),
asyncMiddleware(removeAllWebVideoFilesController)
)
filesRouter.delete(
[ '/:id/webtorrent/:videoFileId', '/:id/web-videos/:videoFileId' ], // TODO: remove webtorrent in V7
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
asyncMiddleware(videoFilesDeleteWebVideoFileValidator),
asyncMiddleware(removeWebVideoFileController)
)
// ---------------------------------------------------------------------------
export {
filesRouter
}
// ---------------------------------------------------------------------------
async function getVideoFileMetadata (req: express.Request, res: express.Response) {
const videoFile = await VideoFileModel.loadWithMetadata(validator.default.toInt(req.params.videoFileId))
return res.json(videoFile.metadata)
}
// ---------------------------------------------------------------------------
async function removeHLSPlaylistController (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
logger.info('Deleting HLS playlist of %s.', video.url, lTags(video.uuid))
await removeHLSPlaylist(video)
await federateVideoIfNeeded(video, false, undefined)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function removeHLSFileController (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const videoFileId = +req.params.videoFileId
logger.info('Deleting HLS file %d of %s.', videoFileId, video.url, lTags(video.uuid))
const playlist = await removeHLSFile(video, videoFileId)
if (playlist) await updatePlaylistAfterFileChange(video, playlist)
await federateVideoIfNeeded(video, false, undefined)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
// ---------------------------------------------------------------------------
async function removeAllWebVideoFilesController (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
logger.info('Deleting Web Video files of %s.', video.url, lTags(video.uuid))
await removeAllWebVideoFiles(video)
await federateVideoIfNeeded(video, false, undefined)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function removeWebVideoFileController (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const videoFileId = +req.params.videoFileId
logger.info('Deleting Web Video file %d of %s.', videoFileId, video.url, lTags(video.uuid))
await removeWebVideoFile(video, videoFileId)
await federateVideoIfNeeded(video, false, undefined)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+272
ファイルの表示
@@ -0,0 +1,272 @@
import express from 'express'
import { move } from 'fs-extra/esm'
import { readFile } from 'fs/promises'
import { decode } from 'magnet-uri'
import parseTorrent, { Instance } from 'parse-torrent'
import { join } from 'path'
import { buildVideoFromImport, buildYoutubeDLImport, insertFromImportIntoDB, YoutubeDlImportError } from '@server/lib/video-pre-import.js'
import { MThumbnail, MVideoThumbnail } from '@server/types/models/index.js'
import {
HttpStatusCode,
ServerErrorCode,
ThumbnailType,
VideoImportCreate,
VideoImportPayload,
VideoImportState
} from '@peertube/peertube-models'
import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger.js'
import { isArray } from '../../../helpers/custom-validators/misc.js'
import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils.js'
import { logger } from '../../../helpers/logger.js'
import { getSecureTorrentName } from '../../../helpers/utils.js'
import { CONFIG } from '../../../initializers/config.js'
import { MIMETYPES } from '../../../initializers/constants.js'
import { JobQueue } from '../../../lib/job-queue/job-queue.js'
import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
videoImportAddValidator,
videoImportCancelValidator,
videoImportDeleteValidator
} from '../../../middlewares/index.js'
const auditLogger = auditLoggerFactory('video-imports')
const videoImportsRouter = express.Router()
const reqVideoFileImport = createReqFiles(
[ 'thumbnailfile', 'previewfile', 'torrentfile' ],
{ ...MIMETYPES.TORRENT.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT }
)
videoImportsRouter.post('/imports',
authenticate,
reqVideoFileImport,
asyncMiddleware(videoImportAddValidator),
asyncRetryTransactionMiddleware(handleVideoImport)
)
videoImportsRouter.post('/imports/:id/cancel',
authenticate,
asyncMiddleware(videoImportCancelValidator),
asyncRetryTransactionMiddleware(cancelVideoImport)
)
videoImportsRouter.delete('/imports/:id',
authenticate,
asyncMiddleware(videoImportDeleteValidator),
asyncRetryTransactionMiddleware(deleteVideoImport)
)
// ---------------------------------------------------------------------------
export {
videoImportsRouter
}
// ---------------------------------------------------------------------------
async function deleteVideoImport (req: express.Request, res: express.Response) {
const videoImport = res.locals.videoImport
await videoImport.destroy()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function cancelVideoImport (req: express.Request, res: express.Response) {
const videoImport = res.locals.videoImport
videoImport.state = VideoImportState.CANCELLED
await videoImport.save()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
function handleVideoImport (req: express.Request, res: express.Response) {
if (req.body.targetUrl) return handleYoutubeDlImport(req, res)
const file = req.files?.['torrentfile']?.[0]
if (req.body.magnetUri || file) return handleTorrentImport(req, res, file)
}
async function handleTorrentImport (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
const body: VideoImportCreate = req.body
const user = res.locals.oauth.token.User
let videoName: string
let torrentName: string
let magnetUri: string
if (torrentfile) {
const result = await processTorrentOrAbortRequest(req, res, torrentfile)
if (!result) return
videoName = result.name
torrentName = result.torrentName
} else {
const result = processMagnetURI(body)
magnetUri = result.magnetUri
videoName = result.name
}
const video = await buildVideoFromImport({
channelId: res.locals.videoChannel.id,
importData: { name: videoName },
importDataOverride: body,
importType: 'torrent'
})
const thumbnailModel = await processThumbnail(req, video)
const previewModel = await processPreview(req, video)
const videoImport = await insertFromImportIntoDB({
video,
thumbnailModel,
previewModel,
videoChannel: res.locals.videoChannel,
tags: body.tags || undefined,
user,
videoPasswords: body.videoPasswords,
videoImportAttributes: {
magnetUri,
torrentName,
state: VideoImportState.PENDING,
userId: user.id
}
})
const payload: VideoImportPayload = {
type: torrentfile
? 'torrent-file'
: 'magnet-uri',
videoImportId: videoImport.id,
preventException: false,
generateTranscription: body.generateTranscription
}
await JobQueue.Instance.createJob({ type: 'video-import', payload })
auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
return res.json(videoImport.toFormattedJSON()).end()
}
function statusFromYtDlImportError (err: YoutubeDlImportError): number {
switch (err.code) {
case YoutubeDlImportError.CODE.NOT_ONLY_UNICAST_URL:
return HttpStatusCode.FORBIDDEN_403
case YoutubeDlImportError.CODE.FETCH_ERROR:
return HttpStatusCode.BAD_REQUEST_400
default:
return HttpStatusCode.INTERNAL_SERVER_ERROR_500
}
}
async function handleYoutubeDlImport (req: express.Request, res: express.Response) {
const body: VideoImportCreate = req.body
const targetUrl = body.targetUrl
const user = res.locals.oauth.token.User
try {
const { job, videoImport } = await buildYoutubeDLImport({
targetUrl,
channel: res.locals.videoChannel,
importDataOverride: body,
thumbnailFilePath: req.files?.['thumbnailfile']?.[0].path,
previewFilePath: req.files?.['previewfile']?.[0].path,
user
})
await JobQueue.Instance.createJob(job)
auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
return res.json(videoImport.toFormattedJSON()).end()
} catch (err) {
logger.error('An error occurred while importing the video %s. ', targetUrl, { err })
return res.fail({
message: err.message,
status: statusFromYtDlImportError(err),
data: {
targetUrl
}
})
}
}
async function processThumbnail (req: express.Request, video: MVideoThumbnail) {
const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
if (thumbnailField) {
const thumbnailPhysicalFile = thumbnailField[0]
return updateLocalVideoMiniatureFromExisting({
inputPath: thumbnailPhysicalFile.path,
video,
type: ThumbnailType.MINIATURE,
automaticallyGenerated: false
})
}
return undefined
}
async function processPreview (req: express.Request, video: MVideoThumbnail): Promise<MThumbnail> {
const previewField = req.files ? req.files['previewfile'] : undefined
if (previewField) {
const previewPhysicalFile = previewField[0]
return updateLocalVideoMiniatureFromExisting({
inputPath: previewPhysicalFile.path,
video,
type: ThumbnailType.PREVIEW,
automaticallyGenerated: false
})
}
return undefined
}
async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
const torrentName = torrentfile.originalname
// Rename the torrent to a secured name
const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
await move(torrentfile.path, newTorrentPath, { overwrite: true })
torrentfile.path = newTorrentPath
const buf = await readFile(torrentfile.path)
// FIXME: typings: parseTorrent now returns an async result
const parsedTorrent = await (parseTorrent(buf) as unknown as Promise<Instance>)
if (parsedTorrent.files.length !== 1) {
cleanUpReqFiles(req)
res.fail({
type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT,
message: 'Torrents with only 1 file are supported.'
})
return undefined
}
return {
name: extractNameFromArray(parsedTorrent.name),
torrentName
}
}
function processMagnetURI (body: VideoImportCreate) {
const magnetUri = body.magnetUri
const parsed = decode(magnetUri)
return {
name: extractNameFromArray(parsed.name),
magnetUri
}
}
function extractNameFromArray (name: string | string[]) {
return isArray(name) ? name[0] : name
}
+232
ファイルの表示
@@ -0,0 +1,232 @@
import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { pickCommonVideoQuery } from '@server/helpers/query.js'
import { doJSONRequest } from '@server/helpers/requests.js'
import { openapiOperationDoc } from '@server/middlewares/doc.js'
import { getServerActor } from '@server/models/application/application.js'
import { MVideoAccountLight } from '@server/types/models/index.js'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js'
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils.js'
import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { REMOTE_SCHEME, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { JobQueue } from '../../../lib/job-queue/index.js'
import { Hooks } from '../../../lib/plugins/hooks.js'
import {
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
checkVideoFollowConstraints,
commonVideosFiltersValidator,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
setDefaultVideosSort,
videosCustomGetValidator,
videosGetValidator,
videosRemoveValidator,
videosSortValidator
} from '../../../middlewares/index.js'
import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter/index.js'
import { VideoModel } from '../../../models/video/video.js'
import { blacklistRouter } from './blacklist.js'
import { videoCaptionsRouter } from './captions.js'
import { videoCommentRouter } from './comment.js'
import { filesRouter } from './files.js'
import { videoImportsRouter } from './import.js'
import { liveRouter } from './live.js'
import { ownershipVideoRouter } from './ownership.js'
import { videoPasswordRouter } from './passwords.js'
import { rateVideoRouter } from './rate.js'
import { videoSourceRouter } from './source.js'
import { statsRouter } from './stats.js'
import { storyboardRouter } from './storyboard.js'
import { studioRouter } from './studio.js'
import { tokenRouter } from './token.js'
import { transcodingRouter } from './transcoding.js'
import { updateRouter } from './update.js'
import { uploadRouter } from './upload.js'
import { viewRouter } from './view.js'
import { videoChaptersRouter } from './chapters.js'
const auditLogger = auditLoggerFactory('videos')
const videosRouter = express.Router()
videosRouter.use(apiRateLimiter)
videosRouter.use('/', blacklistRouter)
videosRouter.use('/', statsRouter)
videosRouter.use('/', rateVideoRouter)
videosRouter.use('/', videoCommentRouter)
videosRouter.use('/', studioRouter)
videosRouter.use('/', videoCaptionsRouter)
videosRouter.use('/', videoImportsRouter)
videosRouter.use('/', ownershipVideoRouter)
videosRouter.use('/', viewRouter)
videosRouter.use('/', liveRouter)
videosRouter.use('/', uploadRouter)
videosRouter.use('/', updateRouter)
videosRouter.use('/', filesRouter)
videosRouter.use('/', transcodingRouter)
videosRouter.use('/', tokenRouter)
videosRouter.use('/', videoPasswordRouter)
videosRouter.use('/', storyboardRouter)
videosRouter.use('/', videoSourceRouter)
videosRouter.use('/', videoChaptersRouter)
videosRouter.get('/categories',
openapiOperationDoc({ operationId: 'getCategories' }),
listVideoCategories
)
videosRouter.get('/licences',
openapiOperationDoc({ operationId: 'getLicences' }),
listVideoLicences
)
videosRouter.get('/languages',
openapiOperationDoc({ operationId: 'getLanguages' }),
listVideoLanguages
)
videosRouter.get('/privacies',
openapiOperationDoc({ operationId: 'getPrivacies' }),
listVideoPrivacies
)
videosRouter.get('/',
openapiOperationDoc({ operationId: 'getVideos' }),
paginationValidator,
videosSortValidator,
setDefaultVideosSort,
setDefaultPagination,
optionalAuthenticate,
commonVideosFiltersValidator,
asyncMiddleware(listVideos)
)
// TODO: remove, deprecated in 5.0 now we send the complete description in VideoDetails
videosRouter.get('/:id/description',
openapiOperationDoc({ operationId: 'getVideoDesc' }),
asyncMiddleware(videosGetValidator),
asyncMiddleware(getVideoDescription)
)
videosRouter.get('/:id',
openapiOperationDoc({ operationId: 'getVideo' }),
optionalAuthenticate,
asyncMiddleware(videosCustomGetValidator('for-api')),
asyncMiddleware(checkVideoFollowConstraints),
asyncMiddleware(getVideo)
)
videosRouter.delete('/:id',
openapiOperationDoc({ operationId: 'delVideo' }),
authenticate,
asyncMiddleware(videosRemoveValidator),
asyncRetryTransactionMiddleware(removeVideo)
)
// ---------------------------------------------------------------------------
export {
videosRouter
}
// ---------------------------------------------------------------------------
function listVideoCategories (_req: express.Request, res: express.Response) {
res.json(VIDEO_CATEGORIES)
}
function listVideoLicences (_req: express.Request, res: express.Response) {
res.json(VIDEO_LICENCES)
}
function listVideoLanguages (_req: express.Request, res: express.Response) {
res.json(VIDEO_LANGUAGES)
}
function listVideoPrivacies (_req: express.Request, res: express.Response) {
res.json(VIDEO_PRIVACIES)
}
async function getVideo (req: express.Request, res: express.Response) {
const videoId = res.locals.videoAPI.id
const userId = res.locals.oauth?.token.User.id
const video = await Hooks.wrapObject(res.locals.videoAPI, 'filter:api.video.get.result', { req, id: videoId, userId })
// Filter may return null/undefined value to forbid video access
if (!video) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
if (video.isOutdated()) {
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
}
return res.json(video.toFormattedDetailsJSON())
}
async function getVideoDescription (req: express.Request, res: express.Response) {
const videoInstance = res.locals.videoAll
const description = videoInstance.isOwned()
? videoInstance.description
: await fetchRemoteVideoDescription(videoInstance)
return res.json({ description })
}
async function listVideos (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const query = pickCommonVideoQuery(req.query)
const countVideos = getCountVideos(req)
const apiOptions = await Hooks.wrapObject({
...query,
displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
},
nsfw: buildNSFWFilter(res, query.nsfw),
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos
}, 'filter:api.videos.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoModel.listForApi.bind(VideoModel),
apiOptions,
'filter:api.videos.list.result'
)
return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
}
async function removeVideo (req: express.Request, res: express.Response) {
const videoInstance = res.locals.videoAll
await sequelizeTypescript.transaction(async t => {
await videoInstance.destroy({ transaction: t })
})
auditLogger.delete(getAuditIdFromRes(res), new VideoAuditView(videoInstance.toFormattedDetailsJSON()))
logger.info('Video with name %s and uuid %s deleted.', videoInstance.name, videoInstance.uuid)
Hooks.runAction('action:api.video.deleted', { video: videoInstance, req, res })
return res.type('json')
.status(HttpStatusCode.NO_CONTENT_204)
.end()
}
// ---------------------------------------------------------------------------
// FIXME: Should not exist, we rely on specific API
async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
const host = video.VideoChannel.Account.Actor.Server.host
const path = video.getDescriptionAPIPath()
const url = REMOTE_SCHEME.HTTP + '://' + host + path
const { body } = await doJSONRequest<any>(url)
return body.description || ''
}
+207
ファイルの表示
@@ -0,0 +1,207 @@
import express from 'express'
import {
HttpStatusCode,
LiveVideoCreate,
LiveVideoUpdate,
ThumbnailType,
UserRight,
VideoState
} from '@peertube/peertube-models'
import { exists } from '@server/helpers/custom-validators/misc.js'
import { createReqFiles } from '@server/helpers/express-utils.js'
import { getFormattedObjects } from '@server/helpers/utils.js'
import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants.js'
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import {
videoLiveAddValidator,
videoLiveFindReplaySessionValidator,
videoLiveGetValidator,
videoLiveListSessionsValidator,
videoLiveUpdateValidator
} from '@server/middlewares/validators/videos/video-live.js'
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting.js'
import { VideoLiveSessionModel } from '@server/models/video/video-live-session.js'
import { MVideoLive } from '@server/types/models/index.js'
import { uuidToShort } from '@peertube/peertube-node-utils'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares/index.js'
import { LocalVideoCreator } from '@server/lib/local-video-creator.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('api', 'live')
const liveRouter = express.Router()
const reqVideoFileLive = createReqFiles([ 'thumbnailfile', 'previewfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
liveRouter.post('/live',
authenticate,
reqVideoFileLive,
asyncMiddleware(videoLiveAddValidator),
asyncRetryTransactionMiddleware(addLiveVideo)
)
liveRouter.get('/live/:videoId/sessions',
authenticate,
asyncMiddleware(videoLiveGetValidator),
videoLiveListSessionsValidator,
asyncMiddleware(getLiveVideoSessions)
)
liveRouter.get('/live/:videoId',
optionalAuthenticate,
asyncMiddleware(videoLiveGetValidator),
getLiveVideo
)
liveRouter.put('/live/:videoId',
authenticate,
asyncMiddleware(videoLiveGetValidator),
videoLiveUpdateValidator,
asyncRetryTransactionMiddleware(updateLiveVideo)
)
liveRouter.get('/:videoId/live-session',
asyncMiddleware(videoLiveFindReplaySessionValidator),
getLiveReplaySession
)
// ---------------------------------------------------------------------------
export {
liveRouter
}
// ---------------------------------------------------------------------------
function getLiveVideo (req: express.Request, res: express.Response) {
const videoLive = res.locals.videoLive
return res.json(videoLive.toFormattedJSON(canSeePrivateLiveInformation(res)))
}
function getLiveReplaySession (req: express.Request, res: express.Response) {
const session = res.locals.videoLiveSession
return res.json(session.toFormattedJSON())
}
async function getLiveVideoSessions (req: express.Request, res: express.Response) {
const videoLive = res.locals.videoLive
const data = await VideoLiveSessionModel.listSessionsOfLiveForAPI({ videoId: videoLive.videoId })
return res.json(getFormattedObjects(data, data.length))
}
function canSeePrivateLiveInformation (res: express.Response) {
const user = res.locals.oauth?.token.User
if (!user) return false
if (user.hasRight(UserRight.GET_ANY_LIVE)) return true
const video = res.locals.videoAll
return video.VideoChannel.Account.userId === user.id
}
async function updateLiveVideo (req: express.Request, res: express.Response) {
const body: LiveVideoUpdate = req.body
const video = res.locals.videoAll
const videoLive = res.locals.videoLive
const newReplaySettingModel = await updateReplaySettings(videoLive, body)
if (newReplaySettingModel) videoLive.replaySettingId = newReplaySettingModel.id
else videoLive.replaySettingId = null
if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive
if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode
video.VideoLive = await videoLive.save()
await federateVideoIfNeeded(video, false)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function updateReplaySettings (videoLive: MVideoLive, body: LiveVideoUpdate) {
if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay
// The live replay is not saved anymore, destroy the old model if it existed
if (!videoLive.saveReplay) {
if (videoLive.replaySettingId) {
await VideoLiveReplaySettingModel.removeSettings(videoLive.replaySettingId)
}
return undefined
}
const settingModel = videoLive.replaySettingId
? await VideoLiveReplaySettingModel.load(videoLive.replaySettingId)
: new VideoLiveReplaySettingModel()
if (exists(body.replaySettings.privacy)) settingModel.privacy = body.replaySettings.privacy
return settingModel.save()
}
async function addLiveVideo (req: express.Request, res: express.Response) {
const videoInfo: LiveVideoCreate = req.body
const thumbnails = [ { type: ThumbnailType.MINIATURE, field: 'thumbnailfile' }, { type: ThumbnailType.PREVIEW, field: 'previewfile' } ]
.map(({ type, field }) => {
if (req.files?.[field]?.[0]) {
return {
path: req.files[field][0].path,
type,
automaticallyGenerated: false,
keepOriginal: false
}
}
return {
path: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND,
type,
automaticallyGenerated: true,
keepOriginal: true
}
})
const localVideoCreator = new LocalVideoCreator({
channel: res.locals.videoChannel,
chapters: undefined,
fallbackChapters: {
fromDescription: false,
finalFallback: undefined
},
liveAttributes: pick(videoInfo, [ 'saveReplay', 'permanentLive', 'latencyMode', 'replaySettings' ]),
videoAttributeResultHook: 'filter:api.video.live.video-attribute.result',
lTags,
videoAttributes: {
...videoInfo,
duration: 0,
state: VideoState.WAITING_FOR_LIVE,
isLive: true,
inputFilename: null
},
videoFile: undefined,
user: res.locals.oauth.token.User,
thumbnails
})
const { video } = await localVideoCreator.create()
logger.info('Video live %s with uuid %s created.', videoInfo.name, video.uuid, lTags())
Hooks.runAction('action:api.live-video.created', { video, req, res })
return res.json({
video: {
id: video.id,
shortUUID: uuidToShort(video.uuid),
uuid: video.uuid
}
})
}
+138
ファイルの表示
@@ -0,0 +1,138 @@
import { HttpStatusCode, VideoChangeOwnershipStatus } from '@peertube/peertube-models'
import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
import { MVideoFullLight } from '@server/types/models/index.js'
import express from 'express'
import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { sendUpdateVideo } from '../../../lib/activitypub/send/index.js'
import { changeVideoChannelShare } from '../../../lib/activitypub/share.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
videosAcceptChangeOwnershipValidator,
videosChangeOwnershipValidator,
videosTerminateChangeOwnershipValidator
} from '../../../middlewares/index.js'
import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership.js'
import { VideoChannelModel } from '../../../models/video/video-channel.js'
import { VideoModel } from '../../../models/video/video.js'
const ownershipVideoRouter = express.Router()
ownershipVideoRouter.post('/:videoId/give-ownership',
authenticate,
asyncMiddleware(videosChangeOwnershipValidator),
asyncRetryTransactionMiddleware(giveVideoOwnership)
)
ownershipVideoRouter.get('/ownership',
authenticate,
paginationValidator,
setDefaultPagination,
asyncRetryTransactionMiddleware(listVideoOwnership)
)
ownershipVideoRouter.post('/ownership/:id/accept',
authenticate,
asyncMiddleware(videosTerminateChangeOwnershipValidator),
asyncMiddleware(videosAcceptChangeOwnershipValidator),
asyncRetryTransactionMiddleware(acceptOwnership)
)
ownershipVideoRouter.post('/ownership/:id/refuse',
authenticate,
asyncMiddleware(videosTerminateChangeOwnershipValidator),
asyncRetryTransactionMiddleware(refuseOwnership)
)
// ---------------------------------------------------------------------------
export {
ownershipVideoRouter
}
// ---------------------------------------------------------------------------
async function giveVideoOwnership (req: express.Request, res: express.Response) {
const videoInstance = res.locals.videoAll
const initiatorAccountId = res.locals.oauth.token.User.Account.id
const nextOwner = res.locals.nextOwner
await sequelizeTypescript.transaction(t => {
return VideoChangeOwnershipModel.findOrCreate({
where: {
initiatorAccountId,
nextOwnerAccountId: nextOwner.id,
videoId: videoInstance.id,
status: VideoChangeOwnershipStatus.WAITING
},
defaults: {
initiatorAccountId,
nextOwnerAccountId: nextOwner.id,
videoId: videoInstance.id,
status: VideoChangeOwnershipStatus.WAITING
},
transaction: t
})
})
logger.info('Ownership change for video %s created.', videoInstance.name)
return res.type('json')
.status(HttpStatusCode.NO_CONTENT_204)
.end()
}
async function listVideoOwnership (req: express.Request, res: express.Response) {
const currentAccountId = res.locals.oauth.token.User.Account.id
const resultList = await VideoChangeOwnershipModel.listForApi(
currentAccountId,
req.query.start || 0,
req.query.count || 10,
req.query.sort || 'createdAt'
)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
function acceptOwnership (req: express.Request, res: express.Response) {
return sequelizeTypescript.transaction(async t => {
const videoChangeOwnership = res.locals.videoChangeOwnership
const channel = res.locals.videoChannel
// We need more attributes for federation
const targetVideo = await VideoModel.loadFull(videoChangeOwnership.Video.id, t)
const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId, t)
targetVideo.channelId = channel.id
const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight
targetVideoUpdated.VideoChannel = channel
if (canVideoBeFederated(targetVideoUpdated)) {
await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t)
await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor)
}
videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED
await videoChangeOwnership.save({ transaction: t })
return res.status(HttpStatusCode.NO_CONTENT_204).end()
})
}
function refuseOwnership (req: express.Request, res: express.Response) {
return sequelizeTypescript.transaction(async t => {
const videoChangeOwnership = res.locals.videoChangeOwnership
videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED
await videoChangeOwnership.save({ transaction: t })
return res.status(HttpStatusCode.NO_CONTENT_204).end()
})
}
+104
ファイルの表示
@@ -0,0 +1,104 @@
import express from 'express'
import { Transaction } from 'sequelize'
import { HttpStatusCode } from '@peertube/peertube-models'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { getVideoWithAttributes } from '@server/helpers/video.js'
import { VideoPasswordModel } from '@server/models/video/video-password.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
setDefaultPagination,
setDefaultSort
} from '../../../middlewares/index.js'
import {
listVideoPasswordValidator,
paginationValidator,
removeVideoPasswordValidator,
updateVideoPasswordListValidator,
videoPasswordsSortValidator
} from '../../../middlewares/validators/index.js'
const lTags = loggerTagsFactory('api', 'video')
const videoPasswordRouter = express.Router()
videoPasswordRouter.get('/:videoId/passwords',
authenticate,
paginationValidator,
videoPasswordsSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listVideoPasswordValidator),
asyncMiddleware(listVideoPasswords)
)
videoPasswordRouter.put('/:videoId/passwords',
authenticate,
asyncMiddleware(updateVideoPasswordListValidator),
asyncMiddleware(updateVideoPasswordList)
)
videoPasswordRouter.delete('/:videoId/passwords/:passwordId',
authenticate,
asyncMiddleware(removeVideoPasswordValidator),
asyncRetryTransactionMiddleware(removeVideoPassword)
)
// ---------------------------------------------------------------------------
export {
videoPasswordRouter
}
// ---------------------------------------------------------------------------
async function listVideoPasswords (req: express.Request, res: express.Response) {
const options = {
videoId: res.locals.videoAll.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort
}
const resultList = await VideoPasswordModel.listPasswords(options)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function updateVideoPasswordList (req: express.Request, res: express.Response) {
const videoInstance = getVideoWithAttributes(res)
const videoId = videoInstance.id
const passwordArray = req.body.passwords as string[]
await VideoPasswordModel.sequelize.transaction(async (t: Transaction) => {
await VideoPasswordModel.deleteAllPasswords(videoId, t)
await VideoPasswordModel.addPasswords(passwordArray, videoId, t)
})
logger.info(
`Video passwords for video with name %s and uuid %s have been updated`,
videoInstance.name,
videoInstance.uuid,
lTags(videoInstance.uuid)
)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function removeVideoPassword (req: express.Request, res: express.Response) {
const videoInstance = getVideoWithAttributes(res)
const password = res.locals.videoPassword
await VideoPasswordModel.deletePassword(password.id)
logger.info(
'Password with id %d of video named %s and uuid %s has been deleted.',
password.id,
videoInstance.name,
videoInstance.uuid,
lTags(videoInstance.uuid)
)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+36
ファイルの表示
@@ -0,0 +1,36 @@
import express from 'express'
import { HttpStatusCode, UserVideoRateUpdate } from '@peertube/peertube-models'
import { logger } from '../../../helpers/logger.js'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares/index.js'
import { userRateVideo } from '@server/lib/rate.js'
const rateVideoRouter = express.Router()
rateVideoRouter.put('/:id/rate',
authenticate,
asyncMiddleware(videoUpdateRateValidator),
asyncRetryTransactionMiddleware(rateVideo)
)
// ---------------------------------------------------------------------------
export {
rateVideoRouter
}
// ---------------------------------------------------------------------------
async function rateVideo (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const video = res.locals.videoAll
await userRateVideo({
account: user.Account,
rateType: (req.body as UserVideoRateUpdate).rating,
video
})
logger.info('Account video rate for video %s of account %s updated.', video.name, user.username)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
+216
ファイルの表示
@@ -0,0 +1,216 @@
import { buildAspectRatio } from '@peertube/peertube-core-utils'
import { HttpStatusCode, UserRight, VideoState } from '@peertube/peertube-models'
import { sequelizeTypescript } from '@server/initializers/database.js'
import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue/index.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js'
import { setupUploadResumableRoutes } from '@server/lib/uploadx.js'
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist.js'
import { buildNewFile, createVideoSource } from '@server/lib/video-file.js'
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { buildNextVideoState } from '@server/lib/video-state.js'
import { openapiOperationDoc } from '@server/middlewares/doc.js'
import { VideoModel } from '@server/models/video/video.js'
import { MStreamingPlaylistFiles, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
import express from 'express'
import { move } from 'fs-extra/esm'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
import {
asyncMiddleware,
authenticate,
ensureUserHasRight,
replaceVideoSourceResumableInitValidator,
replaceVideoSourceResumableValidator,
videoSourceGetLatestValidator
} from '../../../middlewares/index.js'
const lTags = loggerTagsFactory('api', 'video')
const videoSourceRouter = express.Router()
videoSourceRouter.get('/:id/source',
openapiOperationDoc({ operationId: 'getVideoSource' }),
authenticate,
asyncMiddleware(videoSourceGetLatestValidator),
getVideoLatestSource
)
videoSourceRouter.delete('/:id/source/file',
openapiOperationDoc({ operationId: 'deleteVideoSourceFile' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
asyncMiddleware(videoSourceGetLatestValidator),
asyncMiddleware(deleteVideoLatestSourceFile)
)
setupUploadResumableRoutes({
routePath: '/:id/source/replace-resumable',
router: videoSourceRouter,
uploadInitAfterMiddlewares: [ asyncMiddleware(replaceVideoSourceResumableInitValidator) ],
uploadedMiddlewares: [ asyncMiddleware(replaceVideoSourceResumableValidator) ],
uploadedController: asyncMiddleware(replaceVideoSourceResumable)
})
// ---------------------------------------------------------------------------
export {
videoSourceRouter
}
// ---------------------------------------------------------------------------
async function deleteVideoLatestSourceFile (req: express.Request, res: express.Response) {
const videoSource = res.locals.videoSource
const video = res.locals.videoAll
await video.removeOriginalFile(videoSource)
videoSource.keptOriginalFilename = null
videoSource.storage = null
await videoSource.save()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
function getVideoLatestSource (req: express.Request, res: express.Response) {
return res.json(res.locals.videoSource.toFormattedJSON())
}
async function replaceVideoSourceResumable (req: express.Request, res: express.Response) {
const videoPhysicalFile = res.locals.updateVideoFileResumable
const user = res.locals.oauth.token.User
const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video', ffprobe: res.locals.ffprobe })
const originalFilename = videoPhysicalFile.originalname
const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(res.locals.videoAll.uuid)
try {
const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(res.locals.videoAll, videoFile)
await move(videoPhysicalFile.path, destination)
let oldWebVideoFiles: MVideoFile[] = []
let oldStreamingPlaylists: MStreamingPlaylistFiles[] = []
const inputFileUpdatedAt = new Date()
const video = await sequelizeTypescript.transaction(async transaction => {
const video = await VideoModel.loadFull(res.locals.videoAll.id, transaction)
oldWebVideoFiles = video.VideoFiles
oldStreamingPlaylists = video.VideoStreamingPlaylists
for (const file of video.VideoFiles) {
await file.destroy({ transaction })
}
for (const playlist of oldStreamingPlaylists) {
await playlist.destroy({ transaction })
}
videoFile.videoId = video.id
await videoFile.save({ transaction })
video.VideoFiles = [ videoFile ]
video.VideoStreamingPlaylists = []
video.state = buildNextVideoState()
video.duration = videoPhysicalFile.duration
video.inputFileUpdatedAt = inputFileUpdatedAt
video.aspectRatio = buildAspectRatio({ width: videoFile.width, height: videoFile.height })
await video.save({ transaction })
await autoBlacklistVideoIfNeeded({
video,
user,
isRemote: false,
isNew: false,
isNewFile: true,
transaction
})
return video
})
await removeOldFiles({ video, files: oldWebVideoFiles, playlists: oldStreamingPlaylists })
const source = await createVideoSource({
inputFilename: originalFilename,
inputProbe: res.locals.ffprobe,
inputPath: destination,
video,
createdAt: inputFileUpdatedAt
})
await regenerateMiniaturesIfNeeded(video, res.locals.ffprobe)
await video.VideoChannel.setAsUpdated()
await addVideoJobsAfterUpload(video, video.getMaxQualityFile())
logger.info('Replaced video file of video %s with uuid %s.', video.name, video.uuid, lTags(video.uuid))
Hooks.runAction('action:api.video.file-updated', { video, req, res })
return res.json(source.toFormattedJSON())
} finally {
videoFileMutexReleaser()
}
}
async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) {
const jobs: (CreateJobArgument & CreateJobOptions)[] = [
{
type: 'manage-video-torrent' as 'manage-video-torrent',
payload: {
videoId: video.id,
videoFileId: videoFile.id,
action: 'create'
}
},
buildStoryboardJobIfNeeded({ video, federate: false }),
{
type: 'federate-video' as 'federate-video',
payload: {
videoUUID: video.uuid,
isNewVideoForFederation: false
}
}
]
if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
jobs.push(await buildMoveJob({ video, isNewVideo: false, previousVideoState: undefined, type: 'move-to-object-storage' }))
}
if (video.state === VideoState.TO_TRANSCODE) {
jobs.push({
type: 'transcoding-job-builder' as 'transcoding-job-builder',
payload: {
videoUUID: video.uuid,
optimizeJob: {
isNewVideo: false
}
}
})
}
return JobQueue.Instance.createSequentialJobFlow(...jobs)
}
async function removeOldFiles (options: {
video: MVideo
files: MVideoFile[]
playlists: MStreamingPlaylistFiles[]
}) {
const { video, files, playlists } = options
for (const file of files) {
await video.removeWebVideoFile(file)
}
for (const playlist of playlists) {
await video.removeStreamingPlaylistFiles(playlist)
}
}
+75
ファイルの表示
@@ -0,0 +1,75 @@
import express from 'express'
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer.js'
import { VideoStatsOverallQuery, VideoStatsTimeserieMetric, VideoStatsTimeserieQuery } from '@peertube/peertube-models'
import {
asyncMiddleware,
authenticate,
videoOverallStatsValidator,
videoRetentionStatsValidator,
videoTimeserieStatsValidator
} from '../../../middlewares/index.js'
const statsRouter = express.Router()
statsRouter.get('/:videoId/stats/overall',
authenticate,
asyncMiddleware(videoOverallStatsValidator),
asyncMiddleware(getOverallStats)
)
statsRouter.get('/:videoId/stats/timeseries/:metric',
authenticate,
asyncMiddleware(videoTimeserieStatsValidator),
asyncMiddleware(getTimeserieStats)
)
statsRouter.get('/:videoId/stats/retention',
authenticate,
asyncMiddleware(videoRetentionStatsValidator),
asyncMiddleware(getRetentionStats)
)
// ---------------------------------------------------------------------------
export {
statsRouter
}
// ---------------------------------------------------------------------------
async function getOverallStats (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const query = req.query as VideoStatsOverallQuery
const stats = await LocalVideoViewerModel.getOverallStats({
video,
startDate: query.startDate,
endDate: query.endDate
})
return res.json(stats)
}
async function getRetentionStats (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const stats = await LocalVideoViewerModel.getRetentionStats(video)
return res.json(stats)
}
async function getTimeserieStats (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const metric = req.params.metric as VideoStatsTimeserieMetric
const query = req.query as VideoStatsTimeserieQuery
const stats = await LocalVideoViewerModel.getTimeserieStats({
video,
metric,
startDate: query.startDate ?? video.createdAt.toISOString(),
endDate: query.endDate ?? new Date().toISOString()
})
return res.json(stats)
}
+29
ファイルの表示
@@ -0,0 +1,29 @@
import express from 'express'
import { getVideoWithAttributes } from '@server/helpers/video.js'
import { StoryboardModel } from '@server/models/video/storyboard.js'
import { asyncMiddleware, videosGetValidator } from '../../../middlewares/index.js'
const storyboardRouter = express.Router()
storyboardRouter.get('/:id/storyboards',
asyncMiddleware(videosGetValidator),
asyncMiddleware(listStoryboards)
)
// ---------------------------------------------------------------------------
export {
storyboardRouter
}
// ---------------------------------------------------------------------------
async function listStoryboards (req: express.Request, res: express.Response) {
const video = getVideoWithAttributes(res)
const storyboards = await StoryboardModel.listStoryboardsOf(video)
return res.json({
storyboards: storyboards.map(s => s.toFormattedJSON())
})
}
+143
ファイルの表示
@@ -0,0 +1,143 @@
import Bluebird from 'bluebird'
import express from 'express'
import { move } from 'fs-extra/esm'
import { basename } from 'path'
import { createAnyReqFiles } from '@server/helpers/express-utils.js'
import { MIMETYPES, VIDEO_FILTERS } from '@server/initializers/constants.js'
import { buildTaskFileFieldname, createVideoStudioJob, getStudioTaskFilePath, getTaskFileFromReq } from '@server/lib/video-studio.js'
import {
HttpStatusCode,
VideoState,
VideoStudioCreateEdition,
VideoStudioTask,
VideoStudioTaskCut,
VideoStudioTaskIntro,
VideoStudioTaskOutro,
VideoStudioTaskPayload,
VideoStudioTaskWatermark
} from '@peertube/peertube-models'
import { asyncMiddleware, authenticate, videoStudioAddEditionValidator } from '../../../middlewares/index.js'
const studioRouter = express.Router()
const tasksFiles = createAnyReqFiles(
MIMETYPES.VIDEO.MIMETYPE_EXT,
(req: express.Request, file: Express.Multer.File, cb: (err: Error, result?: boolean) => void) => {
const body = req.body as VideoStudioCreateEdition
// Fetch array element
const matches = file.fieldname.match(/tasks\[(\d+)\]/)
if (!matches) return cb(new Error('Cannot find array element indice for ' + file.fieldname))
const indice = parseInt(matches[1])
const task = body.tasks[indice]
if (!task) return cb(new Error('Cannot find array element of indice ' + indice + ' for ' + file.fieldname))
if (
[ 'add-intro', 'add-outro', 'add-watermark' ].includes(task.name) &&
file.fieldname === buildTaskFileFieldname(indice)
) {
return cb(null, true)
}
return cb(null, false)
}
)
studioRouter.post('/:videoId/studio/edit',
authenticate,
tasksFiles,
asyncMiddleware(videoStudioAddEditionValidator),
asyncMiddleware(createEditionTasks)
)
// ---------------------------------------------------------------------------
export {
studioRouter
}
// ---------------------------------------------------------------------------
async function createEditionTasks (req: express.Request, res: express.Response) {
const files = req.files as Express.Multer.File[]
const body = req.body as VideoStudioCreateEdition
const video = res.locals.videoAll
video.state = VideoState.TO_EDIT
await video.save()
const payload = {
videoUUID: video.uuid,
tasks: await Bluebird.mapSeries(body.tasks, (t, i) => buildTaskPayload(t, i, files))
}
await createVideoStudioJob({
user: res.locals.oauth.token.User,
payload,
video
})
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
const taskPayloadBuilders: {
[id in VideoStudioTask['name']]: (
task: VideoStudioTask,
indice?: number,
files?: Express.Multer.File[]
) => Promise<VideoStudioTaskPayload>
} = {
'add-intro': buildIntroOutroTask,
'add-outro': buildIntroOutroTask,
'cut': buildCutTask,
'add-watermark': buildWatermarkTask
}
function buildTaskPayload (task: VideoStudioTask, indice: number, files: Express.Multer.File[]): Promise<VideoStudioTaskPayload> {
return taskPayloadBuilders[task.name](task, indice, files)
}
async function buildIntroOutroTask (task: VideoStudioTaskIntro | VideoStudioTaskOutro, indice: number, files: Express.Multer.File[]) {
const destination = await moveStudioFileToPersistentTMP(getTaskFileFromReq(files, indice).path)
return {
name: task.name,
options: {
file: destination
}
}
}
function buildCutTask (task: VideoStudioTaskCut) {
return Promise.resolve({
name: task.name,
options: {
start: task.options.start,
end: task.options.end
}
})
}
async function buildWatermarkTask (task: VideoStudioTaskWatermark, indice: number, files: Express.Multer.File[]) {
const destination = await moveStudioFileToPersistentTMP(getTaskFileFromReq(files, indice).path)
return {
name: task.name,
options: {
file: destination,
watermarkSizeRatio: VIDEO_FILTERS.WATERMARK.SIZE_RATIO,
horitonzalMarginRatio: VIDEO_FILTERS.WATERMARK.HORIZONTAL_MARGIN_RATIO,
verticalMarginRatio: VIDEO_FILTERS.WATERMARK.VERTICAL_MARGIN_RATIO
}
}
}
async function moveStudioFileToPersistentTMP (file: string) {
const destination = getStudioTaskFilePath(basename(file))
await move(file, destination)
return destination
}
+33
ファイルの表示
@@ -0,0 +1,33 @@
import express from 'express'
import { VideoTokensManager } from '@server/lib/video-tokens-manager.js'
import { VideoPrivacy, VideoToken } from '@peertube/peertube-models'
import { asyncMiddleware, optionalAuthenticate, videoFileTokenValidator, videosCustomGetValidator } from '../../../middlewares/index.js'
const tokenRouter = express.Router()
tokenRouter.post('/:id/token',
optionalAuthenticate,
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
videoFileTokenValidator,
generateToken
)
// ---------------------------------------------------------------------------
export {
tokenRouter
}
// ---------------------------------------------------------------------------
function generateToken (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo
const files = video.privacy === VideoPrivacy.PASSWORD_PROTECTED
? VideoTokensManager.Instance.createForPasswordProtectedVideo({ videoUUID: video.uuid })
: VideoTokensManager.Instance.createForAuthUser({ videoUUID: video.uuid, user: res.locals.oauth.token.User })
return res.json({
files
} as VideoToken)
}
+60
ファイルの表示
@@ -0,0 +1,60 @@
import express from 'express'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { createTranscodingJobs } from '@server/lib/transcoding/create-transcoding-job.js'
import { computeResolutionsToTranscode } from '@server/lib/transcoding/transcoding-resolutions.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@peertube/peertube-models'
import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares/index.js'
const lTags = loggerTagsFactory('api', 'video')
const transcodingRouter = express.Router()
transcodingRouter.post('/:videoId/transcoding',
authenticate,
ensureUserHasRight(UserRight.RUN_VIDEO_TRANSCODING),
asyncMiddleware(createTranscodingValidator),
asyncMiddleware(createTranscoding)
)
// ---------------------------------------------------------------------------
export {
transcodingRouter
}
// ---------------------------------------------------------------------------
async function createTranscoding (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
logger.info('Creating %s transcoding job for %s.', req.body.transcodingType, video.url, lTags())
const body: VideoTranscodingCreate = req.body
await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingTranscode')
const { resolution: maxResolution, hasAudio } = await video.probeMaxQualityFile()
const resolutions = await Hooks.wrapObject(
computeResolutionsToTranscode({ input: maxResolution, type: 'vod', includeInput: true, strictLower: false, hasAudio }),
'filter:transcoding.manual.resolutions-to-transcode.result',
body
)
if (resolutions.length === 0) {
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
video.state = VideoState.TO_TRANSCODE
await video.save()
await createTranscodingJobs({
video,
resolutions,
transcodingType: body.transcodingType,
isNewVideo: false,
user: null // Don't specify priority since these transcoding jobs are fired by the admin
})
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}

変更されたファイルが多すぎるため,一部のファイルは表示されません さらに表示