Implement avatar miniatures (#4639)
* client: remove unused file * refactor(client/my-actor-avatar): size from input Read size from component input instead of scss, to make it possible to use smaller avatar images when implemented. * implement avatar miniatures close #4560 * fix(test): max file size * fix(search-index): normalize res acc to avatarMini * refactor avatars to an array * client/search: resize channel avatar to 120 * refactor(client/videos): remove unused function * client(actor-avatar): set default size * fix tests and avatars full result When findOne is used only an array containting one avatar is returned. * update migration version and version notations * server/search: harmonize normalizing * Cleanup avatar miniature PR Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
parent
5cad2ca9db
commit
d0800f7661
150 changed files with 2027 additions and 1276 deletions
|
@ -9,8 +9,11 @@
|
|||
|
||||
<div class="channel-avatar-row">
|
||||
<my-actor-avatar
|
||||
[channel]="videoChannel" [internalHref]="getVideoChannelLink(videoChannel)"
|
||||
i18n-title title="See this video channel"
|
||||
[channel]="videoChannel"
|
||||
[internalHref]="getVideoChannelLink(videoChannel)"
|
||||
i18n-title
|
||||
title="See this video channel"
|
||||
size="75"
|
||||
></my-actor-avatar>
|
||||
|
||||
<h2>
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
grid-template-rows: auto 1fr;
|
||||
|
||||
my-actor-avatar {
|
||||
@include actor-avatar-size(75px);
|
||||
@include margin-right(15px);
|
||||
|
||||
grid-column: 1;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="account-info">
|
||||
|
||||
<div class="account-avatar-row">
|
||||
<my-actor-avatar class="main-avatar" [account]="account"></my-actor-avatar>
|
||||
<my-actor-avatar class="main-avatar" [account]="account" size="120"></my-actor-avatar>
|
||||
|
||||
<div>
|
||||
<div class="section-label" i18n>ACCOUNT</div>
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
<p-table
|
||||
[value]="blockedAccounts" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order"
|
||||
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
||||
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} muted accounts"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div class="ml-auto">
|
||||
<my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 150px;">Action</th> <!-- column for action buttons -->
|
||||
<th style="width: calc(100% - 300px);" i18n>Account</th>
|
||||
<th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-accountBlock>
|
||||
<tr>
|
||||
<td class="action-cell">
|
||||
<button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
|
||||
<div class="chip two-lines">
|
||||
<my-actor-avatar [account]="accountBlock.blockedAccount"></my-actor-avatar>
|
||||
<div>
|
||||
{{ accountBlock.blockedAccount.displayName }}
|
||||
<span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td>{{ accountBlock.createdAt | date: 'short' }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<div class="no-results">
|
||||
<ng-container *ngIf="search" i18n>No account found matching current filters.</ng-container>
|
||||
<ng-container *ngIf="!search" i18n>No account found.</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
|
@ -66,7 +66,7 @@
|
|||
<td>
|
||||
<a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
|
||||
<div class="chip two-lines">
|
||||
<my-actor-avatar [account]="videoComment.account"></my-actor-avatar>
|
||||
<my-actor-avatar [account]="videoComment.account" size="32"></my-actor-avatar>
|
||||
<div>
|
||||
{{ videoComment.account.displayName }}
|
||||
<span>{{ videoComment.by }}</span>
|
||||
|
|
|
@ -111,7 +111,7 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI
|
|||
next: data => {
|
||||
this.notifier.success($localize`Avatar changed.`)
|
||||
|
||||
this.videoChannel.updateAvatar(data.avatar)
|
||||
this.videoChannel.updateAvatar(data.avatars)
|
||||
},
|
||||
|
||||
error: (err: HttpErrorResponse) => genericUploadErrorHandler({
|
||||
|
@ -141,7 +141,7 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI
|
|||
next: data => {
|
||||
this.notifier.success($localize`Banner changed.`)
|
||||
|
||||
this.videoChannel.updateBanner(data.banner)
|
||||
this.videoChannel.updateBanner(data.banners)
|
||||
},
|
||||
|
||||
error: (err: HttpErrorResponse) => genericUploadErrorHandler({
|
||||
|
|
|
@ -43,7 +43,7 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked {
|
|||
next: data => {
|
||||
this.notifier.success($localize`Avatar changed.`)
|
||||
|
||||
this.user.updateAccountAvatar(data.avatar)
|
||||
this.user.updateAccountAvatar(data.avatars)
|
||||
},
|
||||
|
||||
error: (err: HttpErrorResponse) => genericUploadErrorHandler({
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
<div class="video-channels">
|
||||
<div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel">
|
||||
<my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar>
|
||||
<my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]" size="80"></my-actor-avatar>
|
||||
|
||||
<div class="video-channel-info">
|
||||
<a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
|
||||
|
|
|
@ -24,7 +24,6 @@ my-edit-button {
|
|||
padding-bottom: 0;
|
||||
|
||||
my-actor-avatar {
|
||||
@include actor-avatar-size(80px);
|
||||
@include margin-right(10px);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
|
||||
<div *ngFor="let follow of follows" class="actor">
|
||||
<my-actor-avatar [account]="follow.follower" [href]="follow.follower.url"></my-actor-avatar>
|
||||
<my-actor-avatar [account]="follow.follower" [href]="follow.follower.url" size="40"></my-actor-avatar>
|
||||
|
||||
<div class="actor-info">
|
||||
<a [href]="follow.follower.url" class="actor-names" rel="noopener noreferrer" target="_blank" i18n-title title="Follower page">
|
||||
|
|
|
@ -12,7 +12,7 @@ input[type=text] {
|
|||
}
|
||||
|
||||
.actor {
|
||||
@include actor-row($avatar-size: 40px, $min-height: auto, $separator: true);
|
||||
@include actor-row($min-height: auto, $separator: true);
|
||||
|
||||
.actor-display-name {
|
||||
font-size: 16px;
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
|
||||
<div *ngFor="let videoChannel of videoChannels" class="actor">
|
||||
<my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar>
|
||||
<my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]" size="80"></my-actor-avatar>
|
||||
|
||||
<div class="actor-info">
|
||||
<a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="actor-names" i18n-title title="Channel page">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { SelectChannelItem } from 'src/types/select-options-item.model'
|
||||
import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { AuthService, Notifier } from '@app/core'
|
||||
import { listUserChannels } from '@app/helpers'
|
||||
import { listUserChannelsForSelect } from '@app/helpers'
|
||||
import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
|
||||
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
||||
import { VideoOwnershipService } from '@app/shared/shared-main'
|
||||
|
@ -36,7 +36,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit {
|
|||
ngOnInit () {
|
||||
this.videoChannels = []
|
||||
|
||||
listUserChannels(this.authService)
|
||||
listUserChannelsForSelect(this.authService)
|
||||
.subscribe(channels => this.videoChannels = channels)
|
||||
|
||||
this.buildForm({
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<td>
|
||||
<a [href]="videoChangeOwnership.initiatorAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
|
||||
<div class="chip two-lines">
|
||||
<my-actor-avatar [account]="videoChangeOwnership.initiatorAccount"></my-actor-avatar>
|
||||
<my-actor-avatar [account]="videoChangeOwnership.initiatorAccount" size="32"></my-actor-avatar>
|
||||
<div>
|
||||
{{ videoChangeOwnership.initiatorAccount.displayName }}
|
||||
<span class="text-muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
import { listUserChannels } from '@app/helpers'
|
||||
import { listUserChannelsForSelect } from '@app/helpers'
|
||||
import {
|
||||
setPlaylistChannelValidator,
|
||||
VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR,
|
||||
|
@ -46,7 +46,7 @@ export class MyVideoPlaylistCreateComponent extends MyVideoPlaylistEdit implemen
|
|||
setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy)
|
||||
})
|
||||
|
||||
listUserChannels(this.authService)
|
||||
listUserChannelsForSelect(this.authService)
|
||||
.subscribe(channels => this.userVideoChannels = channels)
|
||||
|
||||
this.serverService.getVideoPlaylistPrivacies()
|
||||
|
|
|
@ -3,7 +3,7 @@ import { map, switchMap } from 'rxjs/operators'
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
import { listUserChannels } from '@app/helpers'
|
||||
import { listUserChannelsForSelect } from '@app/helpers'
|
||||
import {
|
||||
setPlaylistChannelValidator,
|
||||
VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR,
|
||||
|
@ -51,7 +51,7 @@ export class MyVideoPlaylistUpdateComponent extends MyVideoPlaylistEdit implemen
|
|||
setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy)
|
||||
})
|
||||
|
||||
listUserChannels(this.authService)
|
||||
listUserChannelsForSelect(this.authService)
|
||||
.subscribe(channels => this.userVideoChannels = channels)
|
||||
|
||||
this.paramsSub = this.route.params
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<ng-container *ngFor="let result of results">
|
||||
<div *ngIf="isVideoChannel(result)" class="entry video-channel">
|
||||
|
||||
<my-actor-avatar [channel]="result" [internalHref]="getInternalChannelUrl(result)" [href]="getExternalChannelUrl(result)"></my-actor-avatar>
|
||||
<my-actor-avatar [channel]="result" [internalHref]="getInternalChannelUrl(result)" [href]="getExternalChannelUrl(result)" size="120"></my-actor-avatar>
|
||||
|
||||
<div class="video-channel-info">
|
||||
<a *ngIf="!isExternalChannelUrl()" [routerLink]="getInternalChannelUrl(result)" class="video-channel-names">
|
||||
|
|
|
@ -58,10 +58,6 @@
|
|||
max-width: 800px;
|
||||
}
|
||||
|
||||
.video-channel my-actor-avatar {
|
||||
@include build-channel-img-size($video-thumbnail-width);
|
||||
}
|
||||
|
||||
.video-channel-info {
|
||||
flex-grow: 1;
|
||||
margin: 0 10px;
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<div class="section-label" i18n>OWNER ACCOUNT</div>
|
||||
|
||||
<div class="avatar-row">
|
||||
<my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar>
|
||||
<my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()" size="48"></my-actor-avatar>
|
||||
|
||||
<div class="actor-info">
|
||||
<h4>
|
||||
|
@ -51,7 +51,7 @@
|
|||
</ng-template>
|
||||
|
||||
<div class="channel-avatar-row">
|
||||
<my-actor-avatar class="main-avatar" [channel]="videoChannel"></my-actor-avatar>
|
||||
<my-actor-avatar class="main-avatar" [channel]="videoChannel" size="120"></my-actor-avatar>
|
||||
|
||||
<div>
|
||||
<div class="section-label" i18n>VIDEO CHANNEL</div>
|
||||
|
|
|
@ -107,10 +107,6 @@
|
|||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.account-avatar {
|
||||
@include actor-avatar-size(48px);
|
||||
}
|
||||
|
||||
.actor-info {
|
||||
@include margin-left(15px);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { catchError, switchMap, tap } from 'rxjs/operators'
|
|||
import { SelectChannelItem } from 'src/types/select-options-item.model'
|
||||
import { Directive, EventEmitter, OnInit } from '@angular/core'
|
||||
import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core'
|
||||
import { listUserChannels } from '@app/helpers'
|
||||
import { listUserChannelsForSelect } from '@app/helpers'
|
||||
import { FormReactive } from '@app/shared/shared-forms'
|
||||
import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
|
@ -38,7 +38,7 @@ export abstract class VideoSend extends FormReactive implements OnInit {
|
|||
ngOnInit () {
|
||||
this.buildForm({})
|
||||
|
||||
listUserChannels(this.authService)
|
||||
listUserChannelsForSelect(this.authService)
|
||||
.subscribe(channels => {
|
||||
this.userVideoChannels = channels
|
||||
this.firstStepChannelId = this.userVideoChannels[0].id
|
||||
|
|
|
@ -3,7 +3,7 @@ import { map, switchMap } from 'rxjs/operators'
|
|||
import { Injectable } from '@angular/core'
|
||||
import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
|
||||
import { AuthService } from '@app/core'
|
||||
import { listUserChannels } from '@app/helpers'
|
||||
import { listUserChannelsForSelect } from '@app/helpers'
|
||||
import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live'
|
||||
|
||||
|
@ -33,7 +33,7 @@ export class VideoUpdateResolver implements Resolve<any> {
|
|||
.loadCompleteDescription(video.descriptionPath)
|
||||
.pipe(map(description => Object.assign(video, { description }))),
|
||||
|
||||
listUserChannels(this.authService),
|
||||
listUserChannelsForSelect(this.authService),
|
||||
|
||||
this.videoCaptionService
|
||||
.listCaptions(video.id)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div *ngIf="isCommentDisplayed()" class="root-comment" [ngClass]="{ 'is-child': isChild() }">
|
||||
<div class="left">
|
||||
<my-actor-avatar *ngIf="!comment.isDeleted" [href]="comment.account.url" [account]="comment.account"></my-actor-avatar>
|
||||
<my-actor-avatar *ngIf="!comment.isDeleted" [href]="comment.account.url" [account]="comment.account" [size]="isChild() ? '25' : '36'"></my-actor-avatar>
|
||||
<div class="vertical-border"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -25,10 +25,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
my-actor-avatar {
|
||||
@include actor-avatar-size(36px);
|
||||
}
|
||||
|
||||
.comment {
|
||||
flex-grow: 1;
|
||||
// Fix word-wrap with flex
|
||||
|
@ -160,11 +156,6 @@ my-video-comment-add {
|
|||
}
|
||||
|
||||
.is-child {
|
||||
// Reduce avatars size for replies
|
||||
my-actor-avatar {
|
||||
@include actor-avatar-size(25px);
|
||||
}
|
||||
|
||||
.left {
|
||||
@include margin-right(6px);
|
||||
}
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
<my-actor-avatar
|
||||
*ngIf="showChannel"
|
||||
class="channel"
|
||||
[class.main-avatar]="showChannel"
|
||||
[channel]="video.channel"
|
||||
[internalHref]="[ '/c', video.byVideoChannel ]"
|
||||
[title]="channelLinkTitle"
|
||||
size="35"
|
||||
></my-actor-avatar>
|
||||
|
||||
<my-actor-avatar
|
||||
*ngIf="showAccount"
|
||||
class="account"
|
||||
[class.main-avatar]="!showChannel"
|
||||
[class.second-avatar]="showChannel"
|
||||
[account]="video.account"
|
||||
[internalHref]="[ '/a', video.byAccount ]"
|
||||
[title]="accountLinkTitle">
|
||||
[title]="accountLinkTitle"
|
||||
size="35">
|
||||
</my-actor-avatar>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
@use '_mixins' as *;
|
||||
|
||||
@mixin main {
|
||||
@include actor-avatar-size(35px);
|
||||
}
|
||||
|
||||
@mixin secondary {
|
||||
height: 60%;
|
||||
width: 60%;
|
||||
|
@ -14,16 +10,11 @@
|
|||
}
|
||||
|
||||
.wrapper {
|
||||
@include actor-avatar-size(35px);
|
||||
@include margin-right(5px);
|
||||
|
||||
position: relative;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.main-avatar {
|
||||
@include main();
|
||||
}
|
||||
|
||||
.second-avatar {
|
||||
@include secondary();
|
||||
}
|
||||
|
|
|
@ -20,8 +20,4 @@ export class VideoAvatarChannelComponent implements OnInit {
|
|||
this.channelLinkTitle = $localize`${this.video.account.name} (channel page)`
|
||||
this.accountLinkTitle = $localize`${this.video.byAccount} (account page)`
|
||||
}
|
||||
|
||||
isChannelAvatarNull () {
|
||||
return this.video.channel.avatar === null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<div class="section channel videos" *ngFor="let object of overview.channels">
|
||||
<div class="section-title">
|
||||
<a [routerLink]="[ '/c', buildVideoChannelBy(object) ]">
|
||||
<my-actor-avatar [channel]="buildVideoChannel(object)"></my-actor-avatar>
|
||||
<my-actor-avatar [channel]="buildVideoChannel(object)" size="28"></my-actor-avatar>
|
||||
|
||||
<h2 class="section-title">{{ object.channel.displayName }}</h2>
|
||||
</a>
|
||||
|
|
|
@ -52,7 +52,6 @@
|
|||
align-items: center;
|
||||
|
||||
my-actor-avatar {
|
||||
@include actor-avatar-size(28px);
|
||||
@include margin-right(8px);
|
||||
|
||||
font-size: initial;
|
||||
|
|
|
@ -132,8 +132,8 @@ export class User implements UserServerModel {
|
|||
}
|
||||
}
|
||||
|
||||
updateAccountAvatar (newAccountAvatar?: ActorImage) {
|
||||
if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar)
|
||||
updateAccountAvatar (newAccountAvatars?: ActorImage[]) {
|
||||
if (newAccountAvatars) this.account.updateAvatar(newAccountAvatars)
|
||||
else this.account.resetAvatar()
|
||||
}
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ export class UserService {
|
|||
changeAvatar (avatarForm: FormData) {
|
||||
const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
|
||||
|
||||
return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm)
|
||||
return this.authHttp.post<{ avatars: ActorImage[] }>(url, avatarForm)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { minBy } from 'lodash-es'
|
||||
import { first, map } from 'rxjs/operators'
|
||||
import { SelectChannelItem } from 'src/types/select-options-item.model'
|
||||
import { AuthService } from '../../core/auth'
|
||||
|
||||
function listUserChannels (authService: AuthService) {
|
||||
function listUserChannelsForSelect (authService: AuthService) {
|
||||
return authService.userInformationLoaded
|
||||
.pipe(
|
||||
first(),
|
||||
|
@ -23,12 +24,12 @@ function listUserChannels (authService: AuthService) {
|
|||
id: c.id,
|
||||
label: c.displayName,
|
||||
support: c.support,
|
||||
avatarPath: c.avatar?.path
|
||||
avatarPath: minBy(c.avatars, 'width')[0]?.path
|
||||
}) as SelectChannelItem)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
listUserChannels
|
||||
listUserChannelsForSelect
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ export class AccountSetupWarningModalComponent {
|
|||
}
|
||||
|
||||
hasAccountAvatar (user: User) {
|
||||
return !!user.account.avatar
|
||||
return user.account.avatars.length !== 0
|
||||
}
|
||||
|
||||
hasAccountDescription (user: User) {
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<td *ngIf="isAdminView()">
|
||||
<a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
|
||||
<div class="chip two-lines">
|
||||
<my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar>
|
||||
<my-actor-avatar [account]="abuse.reporterAccount" size="32"></my-actor-avatar>
|
||||
<div>
|
||||
{{ abuse.reporterAccount.displayName }}
|
||||
<span>{{ abuse.reporterAccount.nameWithHost }}</span>
|
||||
|
|
|
@ -72,7 +72,7 @@ export class ActorAvatarEditComponent implements OnInit {
|
|||
}
|
||||
|
||||
hasAvatar () {
|
||||
return !!this.preview || !!this.actor.avatar
|
||||
return !!this.preview || this.actor.avatars.length !== 0
|
||||
}
|
||||
|
||||
isChannel () {
|
||||
|
|
|
@ -20,38 +20,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
$sizes: '18', '25', '28', '32', '34', '35', '36', '40', '48', '75', '80', '100', '120';
|
||||
|
||||
@each $size in $sizes {
|
||||
.avatar-#{$size} {
|
||||
--avatarSize: #{$size}px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-18 {
|
||||
--avatarSize: 18px;
|
||||
--initialFontSize: 13px;
|
||||
}
|
||||
|
||||
.avatar-25 {
|
||||
--avatarSize: 25px;
|
||||
}
|
||||
|
||||
.avatar-32 {
|
||||
--avatarSize: 32px;
|
||||
}
|
||||
|
||||
.avatar-34 {
|
||||
--avatarSize: 34px;
|
||||
}
|
||||
|
||||
.avatar-36 {
|
||||
--avatarSize: 36px;
|
||||
}
|
||||
|
||||
.avatar-40 {
|
||||
--avatarSize: 40px;
|
||||
}
|
||||
|
||||
.avatar-100 {
|
||||
--avatarSize: 100px;
|
||||
--initialFontSize: 40px;
|
||||
}
|
||||
|
||||
.avatar-120 {
|
||||
--avatarSize: 120px;
|
||||
--initialFontSize: 46px;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ import { Account } from '../shared-main/account/account.model'
|
|||
|
||||
type ActorInput = {
|
||||
name: string
|
||||
avatar?: { url?: string, path: string }
|
||||
avatars: { width: number, url?: string, path: string }[]
|
||||
url: string
|
||||
}
|
||||
|
||||
export type ActorAvatarSize = '18' | '25' | '32' | '34' | '36' | '40' | '100' | '120'
|
||||
export type ActorAvatarSize = '18' | '25' | '28' | '32' | '34' | '35' | '36' | '40' | '48' | '75' | '80' | '100' | '120'
|
||||
|
||||
@Component({
|
||||
selector: 'my-actor-avatar',
|
||||
|
@ -23,7 +23,7 @@ export class ActorAvatarComponent {
|
|||
|
||||
@Input() previewImage: string
|
||||
|
||||
@Input() size: ActorAvatarSize
|
||||
@Input() size: ActorAvatarSize = '32'
|
||||
|
||||
// Use an external link
|
||||
@Input() href: string
|
||||
|
@ -50,14 +50,13 @@ export class ActorAvatarComponent {
|
|||
}
|
||||
|
||||
get defaultAvatarUrl () {
|
||||
if (this.channel) return VideoChannel.GET_DEFAULT_AVATAR_URL()
|
||||
|
||||
return Account.GET_DEFAULT_AVATAR_URL()
|
||||
if (this.account) return Account.GET_DEFAULT_AVATAR_URL(+this.size)
|
||||
if (this.channel) return VideoChannel.GET_DEFAULT_AVATAR_URL(+this.size)
|
||||
}
|
||||
|
||||
get avatarUrl () {
|
||||
if (this.account) return Account.GET_ACTOR_AVATAR_URL(this.account)
|
||||
if (this.channel) return VideoChannel.GET_ACTOR_AVATAR_URL(this.channel)
|
||||
if (this.account) return Account.GET_ACTOR_AVATAR_URL(this.account, +this.size)
|
||||
if (this.channel) return VideoChannel.GET_ACTOR_AVATAR_URL(this.channel, +this.size)
|
||||
|
||||
return ''
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div *ngIf="channel" class="channel">
|
||||
|
||||
<div class="channel-avatar-row">
|
||||
<my-actor-avatar [channel]="channel" [internalHref]="getVideoChannelLink()" i18n-title title="See this video channel"></my-actor-avatar>
|
||||
<my-actor-avatar [channel]="channel" [internalHref]="getVideoChannelLink()" i18n-title title="See this video channel" size="75"></my-actor-avatar>
|
||||
|
||||
<h6>
|
||||
<a [routerLink]="getVideoChannelLink()" i18n-title title="See this video channel">
|
||||
|
|
|
@ -26,8 +26,6 @@
|
|||
}
|
||||
|
||||
my-actor-avatar {
|
||||
@include actor-avatar-size(75px);
|
||||
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ export class SelectChannelComponent implements ControlValueAccessor, OnChanges {
|
|||
this.channels = this.items.map(c => {
|
||||
const avatarPath = c.avatarPath
|
||||
? c.avatarPath
|
||||
: VideoChannel.GET_DEFAULT_AVATAR_URL()
|
||||
: VideoChannel.GET_DEFAULT_AVATAR_URL(20)
|
||||
|
||||
return Object.assign({}, c, { avatarPath })
|
||||
})
|
||||
|
|
|
@ -17,11 +17,15 @@ export class Account extends Actor implements ServerAccount {
|
|||
|
||||
userId?: number
|
||||
|
||||
static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) {
|
||||
return Actor.GET_ACTOR_AVATAR_URL(actor)
|
||||
static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) {
|
||||
return Actor.GET_ACTOR_AVATAR_URL(actor, size)
|
||||
}
|
||||
|
||||
static GET_DEFAULT_AVATAR_URL () {
|
||||
static GET_DEFAULT_AVATAR_URL (size: number) {
|
||||
if (size <= 48) {
|
||||
return `${window.location.origin}/client/assets/images/default-avatar-account-48x48.png`
|
||||
}
|
||||
|
||||
return `${window.location.origin}/client/assets/images/default-avatar-account.png`
|
||||
}
|
||||
|
||||
|
@ -42,12 +46,12 @@ export class Account extends Actor implements ServerAccount {
|
|||
this.mutedServerByInstance = false
|
||||
}
|
||||
|
||||
updateAvatar (newAvatar: ActorImage) {
|
||||
this.avatar = newAvatar
|
||||
updateAvatar (newAvatars: ActorImage[]) {
|
||||
this.avatars = newAvatars
|
||||
}
|
||||
|
||||
resetAvatar () {
|
||||
this.avatar = null
|
||||
this.avatars = []
|
||||
}
|
||||
|
||||
updateBlockStatus (blockStatus: BlockStatus) {
|
||||
|
|
|
@ -13,20 +13,22 @@ export abstract class Actor implements ServerActor {
|
|||
|
||||
createdAt: Date | string
|
||||
|
||||
avatar: ActorImage
|
||||
// TODO: remove, deprecated in 4.2
|
||||
avatar: never
|
||||
|
||||
avatars: ActorImage[]
|
||||
|
||||
isLocal: boolean
|
||||
|
||||
static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) {
|
||||
if (actor?.avatar?.url) return actor.avatar.url
|
||||
static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) {
|
||||
const avatar = actor.avatars.sort((a, b) => a.width - b.width).find(a => a.width >= size)
|
||||
|
||||
if (actor?.avatar) {
|
||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||
if (!avatar) return ''
|
||||
if (avatar.url) return avatar.url
|
||||
|
||||
return absoluteAPIUrl + actor.avatar.path
|
||||
}
|
||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||
|
||||
return ''
|
||||
return absoluteAPIUrl + avatar.path
|
||||
}
|
||||
|
||||
static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) {
|
||||
|
@ -55,7 +57,7 @@ export abstract class Actor implements ServerActor {
|
|||
|
||||
if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString())
|
||||
|
||||
this.avatar = hash.avatar
|
||||
this.avatars = hash.avatars
|
||||
this.isLocal = Actor.IS_LOCAL(this.host)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ export class ChannelsSetupMessageComponent implements OnInit {
|
|||
hasChannelNotConfigured () {
|
||||
if (!this.user.videoChannels) return false
|
||||
|
||||
return this.user.videoChannels.filter((channel: VideoChannel) => (!channel.avatar || !channel.description)).length > 0
|
||||
return this.user.videoChannels.filter((channel: VideoChannel) => (channel.avatars.length === 0 || !channel.description)).length > 0
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
|
|
|
@ -254,11 +254,11 @@ export class UserNotification implements UserNotificationServer {
|
|||
return [ this.buildVideoUrl(comment.video), { threadId: comment.threadId } ]
|
||||
}
|
||||
|
||||
private setAccountAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) {
|
||||
actor.avatarUrl = Account.GET_ACTOR_AVATAR_URL(actor) || Account.GET_DEFAULT_AVATAR_URL()
|
||||
private setAccountAvatarUrl (actor: { avatarUrl?: string, avatars: { width: number, url?: string, path: string }[] }) {
|
||||
actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor, 48) || Account.GET_DEFAULT_AVATAR_URL(48)
|
||||
}
|
||||
|
||||
private setVideoChannelAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) {
|
||||
actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor) || VideoChannel.GET_DEFAULT_AVATAR_URL()
|
||||
private setVideoChannelAvatarUrl (actor: { avatarUrl?: string, avatars: { width: number, url?: string, path: string }[] }) {
|
||||
actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor, 48) || VideoChannel.GET_DEFAULT_AVATAR_URL(48)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,11 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
|||
nameWithHost: string
|
||||
nameWithHostForced: string
|
||||
|
||||
banner: ActorImage
|
||||
// TODO: remove, deprecated in 4.2
|
||||
banner: never
|
||||
|
||||
banners: ActorImage[]
|
||||
|
||||
bannerUrl: string
|
||||
|
||||
updatedAt: Date | string
|
||||
|
@ -24,23 +28,25 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
|||
|
||||
viewsPerDay?: ViewsPerDate[]
|
||||
|
||||
static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) {
|
||||
return Actor.GET_ACTOR_AVATAR_URL(actor)
|
||||
static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) {
|
||||
return Actor.GET_ACTOR_AVATAR_URL(actor, size)
|
||||
}
|
||||
|
||||
static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) {
|
||||
if (channel?.banner?.url) return channel.banner.url
|
||||
if (!channel) return ''
|
||||
|
||||
if (channel?.banner) {
|
||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||
const banner = channel.banners[0]
|
||||
if (!banner) return ''
|
||||
|
||||
return absoluteAPIUrl + channel.banner.path
|
||||
}
|
||||
|
||||
return ''
|
||||
if (banner.url) return banner.url
|
||||
return getAbsoluteAPIUrl() + banner.path
|
||||
}
|
||||
|
||||
static GET_DEFAULT_AVATAR_URL () {
|
||||
static GET_DEFAULT_AVATAR_URL (size: number) {
|
||||
if (size <= 48) {
|
||||
return `${window.location.origin}/client/assets/images/default-avatar-video-channel-48x48.png`
|
||||
}
|
||||
|
||||
return `${window.location.origin}/client/assets/images/default-avatar-video-channel.png`
|
||||
}
|
||||
|
||||
|
@ -51,7 +57,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
|||
this.description = hash.description
|
||||
this.support = hash.support
|
||||
|
||||
this.banner = hash.banner
|
||||
this.banners = hash.banners
|
||||
|
||||
this.isLocal = hash.isLocal
|
||||
|
||||
|
@ -74,24 +80,24 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
|||
this.updateComputedAttributes()
|
||||
}
|
||||
|
||||
updateAvatar (newAvatar: ActorImage) {
|
||||
this.avatar = newAvatar
|
||||
updateAvatar (newAvatars: ActorImage[]) {
|
||||
this.avatars = newAvatars
|
||||
|
||||
this.updateComputedAttributes()
|
||||
}
|
||||
|
||||
resetAvatar () {
|
||||
this.updateAvatar(null)
|
||||
this.updateAvatar([])
|
||||
}
|
||||
|
||||
updateBanner (newBanner: ActorImage) {
|
||||
this.banner = newBanner
|
||||
updateBanner (newBanners: ActorImage[]) {
|
||||
this.banners = newBanners
|
||||
|
||||
this.updateComputedAttributes()
|
||||
}
|
||||
|
||||
resetBanner () {
|
||||
this.updateBanner(null)
|
||||
this.updateBanner([])
|
||||
}
|
||||
|
||||
updateComputedAttributes () {
|
||||
|
|
|
@ -80,7 +80,7 @@ export class VideoChannelService {
|
|||
changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') {
|
||||
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick'
|
||||
|
||||
return this.authHttp.post<{ avatar?: ActorImage, banner?: ActorImage }>(url, avatarForm)
|
||||
return this.authHttp.post<{ avatars?: ActorImage[], banners?: ActorImage[] }>(url, avatarForm)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,11 @@ export class Video implements VideoServerModel {
|
|||
displayName: string
|
||||
url: string
|
||||
host: string
|
||||
avatar?: ActorImage
|
||||
|
||||
// TODO: remove, deprecated in 4.2
|
||||
avatar: ActorImage
|
||||
|
||||
avatars: ActorImage[]
|
||||
}
|
||||
|
||||
channel: {
|
||||
|
@ -93,7 +97,11 @@ export class Video implements VideoServerModel {
|
|||
displayName: string
|
||||
url: string
|
||||
host: string
|
||||
avatar?: ActorImage
|
||||
|
||||
// TODO: remove, deprecated in 4.2
|
||||
avatar: ActorImage
|
||||
|
||||
avatars: ActorImage[]
|
||||
}
|
||||
|
||||
userHistory?: {
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<td>
|
||||
<a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
|
||||
<div class="chip two-lines">
|
||||
<my-actor-avatar [account]="accountBlock.blockedAccount"></my-actor-avatar>
|
||||
<my-actor-avatar [account]="accountBlock.blockedAccount" size="32"></my-actor-avatar>
|
||||
<div>
|
||||
{{ accountBlock.blockedAccount.displayName }}
|
||||
<span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span>
|
||||
|
|
|
@ -13,11 +13,13 @@
|
|||
<my-actor-avatar
|
||||
*ngIf="displayOptions.avatar && displayOwnerVideoChannel() && !displayAsRow" [title]="channelLinkTitle"
|
||||
[channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]"
|
||||
size="32"
|
||||
></my-actor-avatar>
|
||||
|
||||
<my-actor-avatar
|
||||
*ngIf="displayOptions.avatar && displayOwnerAccount() && !displayAsRow" [title]="channelLinkTitle"
|
||||
[account]="video.account" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]"
|
||||
size="32"
|
||||
></my-actor-avatar>
|
||||
|
||||
<div class="w-100 d-flex flex-column">
|
||||
|
|
BIN
client/src/assets/images/default-avatar-account-48x48.png
Normal file
BIN
client/src/assets/images/default-avatar-account-48x48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 878 B |
BIN
client/src/assets/images/default-avatar-video-channel-48x48.png
Normal file
BIN
client/src/assets/images/default-avatar-video-channel-48x48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 728 B |
|
@ -26,10 +26,6 @@
|
|||
grid-column: 1;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.main-avatar {
|
||||
@include actor-avatar-size(120px);
|
||||
}
|
||||
|
||||
> div {
|
||||
@include margin-left($img-margin);
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
@mixin actor-row ($avatar-size: 80px, $avatar-margin-right: 10px, $min-height: 130px, $separator: true) {
|
||||
@mixin actor-row ($avatar-margin-right: 10px, $min-height: 130px, $separator: true) {
|
||||
@include row-blocks($min-height: $min-height, $separator: $separator);
|
||||
|
||||
> my-actor-avatar {
|
||||
@include actor-avatar-size($avatar-size);
|
||||
|
||||
@include margin-right($avatar-margin-right);
|
||||
}
|
||||
|
||||
|
|
|
@ -887,7 +887,7 @@
|
|||
height: $avatar-height;
|
||||
|
||||
my-actor-avatar {
|
||||
@include actor-avatar-size($avatar-height);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div {
|
||||
|
|
106
scripts/migrations/peertube-4.2.ts
Normal file
106
scripts/migrations/peertube-4.2.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { minBy } from 'lodash'
|
||||
import { join } from 'path'
|
||||
import { processImage } from '@server/helpers/image-utils'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
|
||||
import { updateActorImages } from '@server/lib/activitypub/actors'
|
||||
import { sendUpdateActor } from '@server/lib/activitypub/send'
|
||||
import { getBiggestActorImage } from '@server/lib/actor-image'
|
||||
import { JobQueue } from '@server/lib/job-queue'
|
||||
import { AccountModel } from '@server/models/account/account'
|
||||
import { ActorModel } from '@server/models/actor/actor'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel'
|
||||
import { MAccountDefault, MActorDefault, MChannelDefault } from '@server/types/models'
|
||||
import { getLowercaseExtension } from '@shared/core-utils'
|
||||
import { buildUUID } from '@shared/extra-utils'
|
||||
import { ActorImageType } from '@shared/models'
|
||||
import { initDatabaseModels } from '../../server/initializers/database'
|
||||
|
||||
run()
|
||||
.then(() => process.exit(0))
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
process.exit(-1)
|
||||
})
|
||||
|
||||
async function run () {
|
||||
console.log('Generate avatar miniatures from existing avatars.')
|
||||
|
||||
await initDatabaseModels(true)
|
||||
JobQueue.Instance.init(true)
|
||||
|
||||
const accounts: AccountModel[] = await AccountModel.findAll({
|
||||
include: [
|
||||
{
|
||||
model: ActorModel,
|
||||
required: true,
|
||||
where: {
|
||||
serverId: null
|
||||
}
|
||||
},
|
||||
{
|
||||
model: VideoChannelModel,
|
||||
include: [
|
||||
{
|
||||
model: AccountModel
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
for (const account of accounts) {
|
||||
try {
|
||||
await generateSmallerAvatarIfNeeded(account)
|
||||
} catch (err) {
|
||||
console.error(`Cannot process account avatar ${account.name}`, err)
|
||||
}
|
||||
|
||||
for (const videoChannel of account.VideoChannels) {
|
||||
try {
|
||||
await generateSmallerAvatarIfNeeded(videoChannel)
|
||||
} catch (err) {
|
||||
console.error(`Cannot process channel avatar ${videoChannel.name}`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Generation finished!')
|
||||
}
|
||||
|
||||
async function generateSmallerAvatarIfNeeded (accountOrChannel: MAccountDefault | MChannelDefault) {
|
||||
const avatars = accountOrChannel.Actor.Avatars
|
||||
if (avatars.length !== 1) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Processing ${accountOrChannel.name}.`)
|
||||
|
||||
await generateSmallerAvatar(accountOrChannel.Actor)
|
||||
accountOrChannel.Actor = Object.assign(accountOrChannel.Actor, { Server: null })
|
||||
|
||||
return sendUpdateActor(accountOrChannel, undefined)
|
||||
}
|
||||
|
||||
async function generateSmallerAvatar (actor: MActorDefault) {
|
||||
const bigAvatar = getBiggestActorImage(actor.Avatars)
|
||||
|
||||
const imageSize = minBy(ACTOR_IMAGES_SIZE[ActorImageType.AVATAR], 'width')
|
||||
const sourceFilename = bigAvatar.filename
|
||||
|
||||
const newImageName = buildUUID() + getLowercaseExtension(sourceFilename)
|
||||
const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename)
|
||||
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, newImageName)
|
||||
|
||||
await processImage(source, destination, imageSize, true)
|
||||
|
||||
const actorImageInfo = {
|
||||
name: newImageName,
|
||||
fileUrl: null,
|
||||
height: imageSize.height,
|
||||
width: imageSize.width,
|
||||
onDisk: true
|
||||
}
|
||||
|
||||
await updateActorImages(actor, ActorImageType.AVATAR, [ actorImageInfo ], undefined)
|
||||
}
|
|
@ -18,10 +18,10 @@ import {
|
|||
} from '../../lib/activitypub/url'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
ensureIsLocalChannel,
|
||||
executeIfActivityPub,
|
||||
localAccountValidator,
|
||||
videoChannelsNameWithHostValidator,
|
||||
ensureIsLocalChannel,
|
||||
videosCustomGetValidator,
|
||||
videosShareValidator
|
||||
} from '../../middlewares'
|
||||
|
@ -265,8 +265,8 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
|
|||
const handler = async (start: number, count: number) => {
|
||||
const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
|
||||
return {
|
||||
total: result.count,
|
||||
data: result.rows.map(r => r.url)
|
||||
total: result.total,
|
||||
data: result.data.map(r => r.url)
|
||||
}
|
||||
}
|
||||
const json = await activityPubCollectionPagination(getLocalVideoSharesActivityPubUrl(video), handler, req.query.page)
|
||||
|
@ -301,9 +301,10 @@ async function videoCommentsController (req: express.Request, res: express.Respo
|
|||
|
||||
const handler = async (start: number, count: number) => {
|
||||
const result = await VideoCommentModel.listAndCountByVideoForAP(video, start, count)
|
||||
|
||||
return {
|
||||
total: result.count,
|
||||
data: result.rows.map(r => r.url)
|
||||
total: result.total,
|
||||
data: result.data.map(r => r.url)
|
||||
}
|
||||
}
|
||||
const json = await activityPubCollectionPagination(getLocalVideoCommentsActivityPubUrl(video), handler, req.query.page)
|
||||
|
@ -425,8 +426,8 @@ function videoRates (req: express.Request, rateType: VideoRateType, video: MVide
|
|||
const handler = async (start: number, count: number) => {
|
||||
const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
|
||||
return {
|
||||
total: result.count,
|
||||
data: result.rows.map(r => r.url)
|
||||
total: result.total,
|
||||
data: result.data.map(r => r.url)
|
||||
}
|
||||
}
|
||||
return activityPubCollectionPagination(url, handler, req.query.page)
|
||||
|
|
|
@ -213,7 +213,7 @@ async function listAccountRatings (req: express.Request, res: express.Response)
|
|||
sort: req.query.sort,
|
||||
type: req.query.rating
|
||||
})
|
||||
return res.json(getFormattedObjects(resultList.rows, resultList.count))
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
||||
async function listAccountFollowers (req: express.Request, res: express.Response) {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import 'multer'
|
||||
import express from 'express'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger'
|
||||
import { getBiggestActorImage } from '@server/lib/actor-image'
|
||||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { pick } from '@shared/core-utils'
|
||||
import { ActorImageType, HttpStatusCode, UserUpdateMe, UserVideoQuota, UserVideoRate as FormattedUserVideoRate } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { createReqFiles } from '../../../helpers/express-utils'
|
||||
|
@ -10,7 +12,7 @@ import { CONFIG } from '../../../initializers/config'
|
|||
import { MIMETYPES } from '../../../initializers/constants'
|
||||
import { sequelizeTypescript } from '../../../initializers/database'
|
||||
import { sendUpdateActor } from '../../../lib/activitypub/send'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/local-actor'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor'
|
||||
import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
|
@ -30,7 +32,6 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
|
|||
import { UserModel } from '../../../models/user/user'
|
||||
import { VideoModel } from '../../../models/video/video'
|
||||
import { VideoImportModel } from '../../../models/video/video-import'
|
||||
import { pick } from '@shared/core-utils'
|
||||
|
||||
const auditLogger = auditLoggerFactory('users')
|
||||
|
||||
|
@ -253,9 +254,17 @@ async function updateMyAvatar (req: express.Request, res: express.Response) {
|
|||
|
||||
const userAccount = await AccountModel.load(user.Account.id)
|
||||
|
||||
const avatar = await updateLocalActorImageFile(userAccount, avatarPhysicalFile, ActorImageType.AVATAR)
|
||||
const avatars = await updateLocalActorImageFiles(
|
||||
userAccount,
|
||||
avatarPhysicalFile,
|
||||
ActorImageType.AVATAR
|
||||
)
|
||||
|
||||
return res.json({ avatar: avatar.toFormattedJSON() })
|
||||
return res.json({
|
||||
// TODO: remove, deprecated in 4.2
|
||||
avatar: getBiggestActorImage(avatars).toFormattedJSON(),
|
||||
avatars: avatars.map(avatar => avatar.toFormattedJSON())
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteMyAvatar (req: express.Request, res: express.Response) {
|
||||
|
@ -264,5 +273,5 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) {
|
|||
const userAccount = await AccountModel.load(user.Account.id)
|
||||
await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR)
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.json({ avatars: [] })
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import express from 'express'
|
|||
import { UserNotificationModel } from '@server/models/user/user-notification'
|
||||
import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
|
||||
import { UserNotificationSetting } from '../../../../shared/models/users'
|
||||
import { getFormattedObjects } from '../../../helpers/utils'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
asyncRetryTransactionMiddleware,
|
||||
|
@ -20,6 +19,7 @@ import {
|
|||
} from '../../../middlewares/validators/user-notifications'
|
||||
import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting'
|
||||
import { meRouter } from './me'
|
||||
import { getFormattedObjects } from '@server/helpers/utils'
|
||||
|
||||
const myNotificationsRouter = express.Router()
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import express from 'express'
|
||||
import { pickCommonVideoQuery } from '@server/helpers/query'
|
||||
import { getBiggestActorImage } from '@server/lib/actor-image'
|
||||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { ActorFollowModel } from '@server/models/actor/actor-follow'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
|
@ -16,7 +17,7 @@ import { MIMETYPES } from '../../initializers/constants'
|
|||
import { sequelizeTypescript } from '../../initializers/database'
|
||||
import { sendUpdateActor } from '../../lib/activitypub/send'
|
||||
import { JobQueue } from '../../lib/job-queue'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/local-actor'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor'
|
||||
import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
|
@ -186,11 +187,15 @@ async function updateVideoChannelBanner (req: express.Request, res: express.Resp
|
|||
const videoChannel = res.locals.videoChannel
|
||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
||||
|
||||
const banner = await updateLocalActorImageFile(videoChannel, bannerPhysicalFile, ActorImageType.BANNER)
|
||||
const banners = await updateLocalActorImageFiles(videoChannel, bannerPhysicalFile, ActorImageType.BANNER)
|
||||
|
||||
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||
|
||||
return res.json({ banner: banner.toFormattedJSON() })
|
||||
return res.json({
|
||||
// TODO: remove, deprecated in 4.2
|
||||
banner: getBiggestActorImage(banners).toFormattedJSON(),
|
||||
banners: banners.map(b => b.toFormattedJSON())
|
||||
})
|
||||
}
|
||||
|
||||
async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
|
||||
|
@ -198,11 +203,14 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp
|
|||
const videoChannel = res.locals.videoChannel
|
||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
||||
|
||||
const avatar = await updateLocalActorImageFile(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR)
|
||||
|
||||
const avatars = await updateLocalActorImageFiles(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR)
|
||||
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||
|
||||
return res.json({ avatar: avatar.toFormattedJSON() })
|
||||
return res.json({
|
||||
// TODO: remove, deprecated in 4.2
|
||||
avatar: getBiggestActorImage(avatars).toFormattedJSON(),
|
||||
avatars: avatars.map(a => a.toFormattedJSON())
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
|
||||
|
|
|
@ -68,7 +68,9 @@ const staticClientOverrides = [
|
|||
'assets/images/icons/icon-512x512.png',
|
||||
'assets/images/default-playlist.jpg',
|
||||
'assets/images/default-avatar-account.png',
|
||||
'assets/images/default-avatar-video-channel.png'
|
||||
'assets/images/default-avatar-account-48x48.png',
|
||||
'assets/images/default-avatar-video-channel.png',
|
||||
'assets/images/default-avatar-video-channel-48x48.png'
|
||||
]
|
||||
|
||||
for (const staticClientOverride of staticClientOverrides) {
|
||||
|
|
|
@ -64,7 +64,15 @@ async function getActorImage (req: express.Request, res: express.Response, next:
|
|||
logger.info('Lazy serve remote actor image %s.', image.fileUrl)
|
||||
|
||||
try {
|
||||
await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type })
|
||||
await pushActorImageProcessInQueue({
|
||||
filename: image.filename,
|
||||
fileUrl: image.fileUrl,
|
||||
size: {
|
||||
height: image.height,
|
||||
width: image.width
|
||||
},
|
||||
type: image.type
|
||||
})
|
||||
} catch (err) {
|
||||
logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err })
|
||||
return res.status(HttpStatusCode.NOT_FOUND_404).end()
|
||||
|
|
|
@ -38,6 +38,9 @@ function getContextData (type: ContextType) {
|
|||
sensitive: 'as:sensitive',
|
||||
language: 'sc:inLanguage',
|
||||
|
||||
// TODO: remove in a few versions, introduced in 4.2
|
||||
icons: 'as:icon',
|
||||
|
||||
isLiveBroadcast: 'sc:isLiveBroadcast',
|
||||
liveSaveReplay: {
|
||||
'@type': 'sc:Boolean',
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
VideoTranscodingFPS
|
||||
} from '../../shared/models'
|
||||
import { ActivityPubActorType } from '../../shared/models/activitypub'
|
||||
import { FollowState } from '../../shared/models/actors'
|
||||
import { ActorImageType, FollowState } from '../../shared/models/actors'
|
||||
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
|
||||
import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
|
||||
import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
|
||||
|
@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 680
|
||||
const LAST_MIGRATION_VERSION = 685
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -633,15 +633,23 @@ const PREVIEWS_SIZE = {
|
|||
height: 480,
|
||||
minWidth: 400
|
||||
}
|
||||
const ACTOR_IMAGES_SIZE = {
|
||||
AVATARS: {
|
||||
width: 120,
|
||||
height: 120
|
||||
},
|
||||
BANNERS: {
|
||||
width: 1920,
|
||||
height: 317 // 6/1 ratio
|
||||
}
|
||||
const ACTOR_IMAGES_SIZE: { [key in ActorImageType]: { width: number, height: number }[]} = {
|
||||
[ActorImageType.AVATAR]: [
|
||||
{
|
||||
width: 120,
|
||||
height: 120
|
||||
},
|
||||
{
|
||||
width: 48,
|
||||
height: 48
|
||||
}
|
||||
],
|
||||
[ActorImageType.BANNER]: [
|
||||
{
|
||||
width: 1920,
|
||||
height: 317 // 6/1 ratio
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const EMBED_SIZE = {
|
||||
|
|
62
server/initializers/migrations/0685-multiple-actor-images.ts
Normal file
62
server/initializers/migrations/0685-multiple-actor-images.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
db: any
|
||||
}): Promise<void> {
|
||||
{
|
||||
await utils.queryInterface.addColumn('actorImage', 'actorId', {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'actor',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
}, { transaction: utils.transaction })
|
||||
|
||||
// Avatars
|
||||
{
|
||||
const query = `UPDATE "actorImage" SET "actorId" = (SELECT "id" FROM "actor" WHERE "actor"."avatarId" = "actorImage"."id") ` +
|
||||
`WHERE "type" = 1`
|
||||
await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction })
|
||||
}
|
||||
|
||||
// Banners
|
||||
{
|
||||
const query = `UPDATE "actorImage" SET "actorId" = (SELECT "id" FROM "actor" WHERE "actor"."bannerId" = "actorImage"."id") ` +
|
||||
`WHERE "type" = 2`
|
||||
await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction })
|
||||
}
|
||||
|
||||
// Remove orphans
|
||||
{
|
||||
const query = `DELETE FROM "actorImage" WHERE id NOT IN (` +
|
||||
`SELECT "bannerId" FROM actor WHERE "bannerId" IS NOT NULL ` +
|
||||
`UNION select "avatarId" FROM actor WHERE "avatarId" IS NOT NULL` +
|
||||
`);`
|
||||
|
||||
await utils.sequelize.query(query, { type: Sequelize.QueryTypes.DELETE, transaction: utils.transaction })
|
||||
}
|
||||
|
||||
await utils.queryInterface.changeColumn('actorImage', 'actorId', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false
|
||||
}, { transaction: utils.transaction })
|
||||
|
||||
await utils.queryInterface.removeColumn('actor', 'avatarId', { transaction: utils.transaction })
|
||||
await utils.queryInterface.removeColumn('actor', 'bannerId', { transaction: utils.transaction })
|
||||
}
|
||||
}
|
||||
|
||||
function down () {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
|
@ -12,53 +12,52 @@ type ImageInfo = {
|
|||
onDisk?: boolean
|
||||
}
|
||||
|
||||
async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) {
|
||||
const oldImageModel = type === ActorImageType.AVATAR
|
||||
? actor.Avatar
|
||||
: actor.Banner
|
||||
async function updateActorImages (actor: MActorImages, type: ActorImageType, imagesInfo: ImageInfo[], t: Transaction) {
|
||||
const avatarsOrBanners = type === ActorImageType.AVATAR
|
||||
? actor.Avatars
|
||||
: actor.Banners
|
||||
|
||||
if (oldImageModel) {
|
||||
// Don't update the avatar if the file URL did not change
|
||||
if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor
|
||||
|
||||
try {
|
||||
await oldImageModel.destroy({ transaction: t })
|
||||
|
||||
setActorImage(actor, type, null)
|
||||
} catch (err) {
|
||||
logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
|
||||
}
|
||||
if (imagesInfo.length === 0) {
|
||||
await deleteActorImages(actor, type, t)
|
||||
}
|
||||
|
||||
if (imageInfo) {
|
||||
for (const imageInfo of imagesInfo) {
|
||||
const oldImageModel = (avatarsOrBanners || []).find(i => i.width === imageInfo.width)
|
||||
|
||||
if (oldImageModel) {
|
||||
// Don't update the avatar if the file URL did not change
|
||||
if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) {
|
||||
continue
|
||||
}
|
||||
|
||||
await safeDeleteActorImage(actor, oldImageModel, type, t)
|
||||
}
|
||||
|
||||
const imageModel = await ActorImageModel.create({
|
||||
filename: imageInfo.name,
|
||||
onDisk: imageInfo.onDisk ?? false,
|
||||
fileUrl: imageInfo.fileUrl,
|
||||
height: imageInfo.height,
|
||||
width: imageInfo.width,
|
||||
type
|
||||
type,
|
||||
actorId: actor.id
|
||||
}, { transaction: t })
|
||||
|
||||
setActorImage(actor, type, imageModel)
|
||||
addActorImage(actor, type, imageModel)
|
||||
}
|
||||
|
||||
return actor
|
||||
}
|
||||
|
||||
async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) {
|
||||
async function deleteActorImages (actor: MActorImages, type: ActorImageType, t: Transaction) {
|
||||
try {
|
||||
if (type === ActorImageType.AVATAR) {
|
||||
await actor.Avatar.destroy({ transaction: t })
|
||||
const association = buildAssociationName(type)
|
||||
|
||||
actor.avatarId = null
|
||||
actor.Avatar = null
|
||||
} else {
|
||||
await actor.Banner.destroy({ transaction: t })
|
||||
|
||||
actor.bannerId = null
|
||||
actor.Banner = null
|
||||
for (const image of actor[association]) {
|
||||
await image.destroy({ transaction: t })
|
||||
}
|
||||
|
||||
actor[association] = []
|
||||
} catch (err) {
|
||||
logger.error('Cannot remove old image of actor %s.', actor.url, { err })
|
||||
}
|
||||
|
@ -66,29 +65,37 @@ async function deleteActorImageInstance (actor: MActorImages, type: ActorImageTy
|
|||
return actor
|
||||
}
|
||||
|
||||
async function safeDeleteActorImage (actor: MActorImages, toDelete: MActorImage, type: ActorImageType, t: Transaction) {
|
||||
try {
|
||||
await toDelete.destroy({ transaction: t })
|
||||
|
||||
const association = buildAssociationName(type)
|
||||
actor[association] = actor[association].filter(image => image.id !== toDelete.id)
|
||||
} catch (err) {
|
||||
logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
ImageInfo,
|
||||
|
||||
updateActorImageInstance,
|
||||
deleteActorImageInstance
|
||||
updateActorImages,
|
||||
deleteActorImages
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) {
|
||||
const id = imageModel
|
||||
? imageModel.id
|
||||
: null
|
||||
function addActorImage (actor: MActorImages, type: ActorImageType, imageModel: MActorImage) {
|
||||
const association = buildAssociationName(type)
|
||||
if (!actor[association]) actor[association] = []
|
||||
|
||||
if (type === ActorImageType.AVATAR) {
|
||||
actorModel.avatarId = id
|
||||
actorModel.Avatar = imageModel
|
||||
} else {
|
||||
actorModel.bannerId = id
|
||||
actorModel.Banner = imageModel
|
||||
}
|
||||
|
||||
return actorModel
|
||||
actor[association].push(imageModel)
|
||||
}
|
||||
|
||||
function buildAssociationName (type: ActorImageType) {
|
||||
return type === ActorImageType.AVATAR
|
||||
? 'Avatars'
|
||||
: 'Banners'
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import { ServerModel } from '@server/models/server/server'
|
|||
import { VideoChannelModel } from '@server/models/video/video-channel'
|
||||
import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models'
|
||||
import { ActivityPubActor, ActorImageType } from '@shared/models'
|
||||
import { updateActorImageInstance } from '../image'
|
||||
import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImageInfoFromObject } from './object-to-model-attributes'
|
||||
import { updateActorImages } from '../image'
|
||||
import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImagesInfoFromObject } from './object-to-model-attributes'
|
||||
import { fetchActorFollowsCount } from './url-to-object'
|
||||
|
||||
export class APActorCreator {
|
||||
|
@ -27,11 +27,11 @@ export class APActorCreator {
|
|||
return sequelizeTypescript.transaction(async t => {
|
||||
const server = await this.setServer(actorInstance, t)
|
||||
|
||||
await this.setImageIfNeeded(actorInstance, ActorImageType.AVATAR, t)
|
||||
await this.setImageIfNeeded(actorInstance, ActorImageType.BANNER, t)
|
||||
|
||||
const { actorCreated, created } = await this.saveActor(actorInstance, t)
|
||||
|
||||
await this.setImageIfNeeded(actorCreated, ActorImageType.AVATAR, t)
|
||||
await this.setImageIfNeeded(actorCreated, ActorImageType.BANNER, t)
|
||||
|
||||
await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t)
|
||||
|
||||
if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance
|
||||
|
@ -71,10 +71,10 @@ export class APActorCreator {
|
|||
}
|
||||
|
||||
private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) {
|
||||
const imageInfo = getImageInfoFromObject(this.actorObject, type)
|
||||
if (!imageInfo) return
|
||||
const imagesInfo = getImagesInfoFromObject(this.actorObject, type)
|
||||
if (imagesInfo.length === 0) return
|
||||
|
||||
return updateActorImageInstance(actor as MActorImages, type, imageInfo, t)
|
||||
return updateActorImages(actor as MActorImages, type, imagesInfo, t)
|
||||
}
|
||||
|
||||
private async saveActor (actor: MActor, t: Transaction) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ActorModel } from '@server/models/actor/actor'
|
|||
import { FilteredModelAttributes } from '@server/types'
|
||||
import { getLowercaseExtension } from '@shared/core-utils'
|
||||
import { buildUUID } from '@shared/extra-utils'
|
||||
import { ActivityPubActor, ActorImageType } from '@shared/models'
|
||||
import { ActivityIconObject, ActivityPubActor, ActorImageType } from '@shared/models'
|
||||
|
||||
function getActorAttributesFromObject (
|
||||
actorObject: ActivityPubActor,
|
||||
|
@ -30,33 +30,36 @@ function getActorAttributesFromObject (
|
|||
}
|
||||
}
|
||||
|
||||
function getImageInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) {
|
||||
const mimetypes = MIMETYPES.IMAGE
|
||||
const icon = type === ActorImageType.AVATAR
|
||||
? actorObject.icon
|
||||
function getImagesInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) {
|
||||
const iconsOrImages = type === ActorImageType.AVATAR
|
||||
? actorObject.icons || actorObject.icon
|
||||
: actorObject.image
|
||||
|
||||
if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
|
||||
return normalizeIconOrImage(iconsOrImages).map(iconOrImage => {
|
||||
const mimetypes = MIMETYPES.IMAGE
|
||||
|
||||
let extension: string
|
||||
if (iconOrImage.type !== 'Image' || !isActivityPubUrlValid(iconOrImage.url)) return undefined
|
||||
|
||||
if (icon.mediaType) {
|
||||
extension = mimetypes.MIMETYPE_EXT[icon.mediaType]
|
||||
} else {
|
||||
const tmp = getLowercaseExtension(icon.url)
|
||||
let extension: string
|
||||
|
||||
if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
|
||||
}
|
||||
if (iconOrImage.mediaType) {
|
||||
extension = mimetypes.MIMETYPE_EXT[iconOrImage.mediaType]
|
||||
} else {
|
||||
const tmp = getLowercaseExtension(iconOrImage.url)
|
||||
|
||||
if (!extension) return undefined
|
||||
if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
|
||||
}
|
||||
|
||||
return {
|
||||
name: buildUUID() + extension,
|
||||
fileUrl: icon.url,
|
||||
height: icon.height,
|
||||
width: icon.width,
|
||||
type
|
||||
}
|
||||
if (!extension) return undefined
|
||||
|
||||
return {
|
||||
name: buildUUID() + extension,
|
||||
fileUrl: iconOrImage.url,
|
||||
height: iconOrImage.height,
|
||||
width: iconOrImage.width,
|
||||
type
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getActorDisplayNameFromObject (actorObject: ActivityPubActor) {
|
||||
|
@ -65,6 +68,15 @@ function getActorDisplayNameFromObject (actorObject: ActivityPubActor) {
|
|||
|
||||
export {
|
||||
getActorAttributesFromObject,
|
||||
getImageInfoFromObject,
|
||||
getImagesInfoFromObject,
|
||||
getActorDisplayNameFromObject
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function normalizeIconOrImage (icon: ActivityIconObject | ActivityIconObject[]): ActivityIconObject[] {
|
||||
if (Array.isArray(icon)) return icon
|
||||
if (icon) return [ icon ]
|
||||
|
||||
return []
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import { VideoChannelModel } from '@server/models/video/video-channel'
|
|||
import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models'
|
||||
import { ActivityPubActor, ActorImageType } from '@shared/models'
|
||||
import { getOrCreateAPOwner } from './get'
|
||||
import { updateActorImageInstance } from './image'
|
||||
import { updateActorImages } from './image'
|
||||
import { fetchActorFollowsCount } from './shared'
|
||||
import { getImageInfoFromObject } from './shared/object-to-model-attributes'
|
||||
import { getImagesInfoFromObject } from './shared/object-to-model-attributes'
|
||||
|
||||
export class APActorUpdater {
|
||||
|
||||
|
@ -29,8 +29,8 @@ export class APActorUpdater {
|
|||
}
|
||||
|
||||
async update () {
|
||||
const avatarInfo = getImageInfoFromObject(this.actorObject, ActorImageType.AVATAR)
|
||||
const bannerInfo = getImageInfoFromObject(this.actorObject, ActorImageType.BANNER)
|
||||
const avatarsInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.AVATAR)
|
||||
const bannersInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.BANNER)
|
||||
|
||||
try {
|
||||
await this.updateActorInstance(this.actor, this.actorObject)
|
||||
|
@ -47,8 +47,8 @@ export class APActorUpdater {
|
|||
}
|
||||
|
||||
await runInReadCommittedTransaction(async t => {
|
||||
await updateActorImageInstance(this.actor, ActorImageType.AVATAR, avatarInfo, t)
|
||||
await updateActorImageInstance(this.actor, ActorImageType.BANNER, bannerInfo, t)
|
||||
await updateActorImages(this.actor, ActorImageType.BANNER, bannersInfo, t)
|
||||
await updateActorImages(this.actor, ActorImageType.AVATAR, avatarsInfo, t)
|
||||
})
|
||||
|
||||
await runInReadCommittedTransaction(async t => {
|
||||
|
|
14
server/lib/actor-image.ts
Normal file
14
server/lib/actor-image.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import maxBy from 'lodash/maxBy'
|
||||
|
||||
function getBiggestActorImage <T extends { width: number }> (images: T[]) {
|
||||
const image = maxBy(images, 'width')
|
||||
|
||||
// If width is null, maxBy won't return a value
|
||||
if (!image) return images[0]
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
export {
|
||||
getBiggestActorImage
|
||||
}
|
|
@ -3,6 +3,7 @@ import { readFile } from 'fs-extra'
|
|||
import { join } from 'path'
|
||||
import validator from 'validator'
|
||||
import { toCompleteUUID } from '@server/helpers/custom-validators/misc'
|
||||
import { ActorImageModel } from '@server/models/actor/actor-image'
|
||||
import { root } from '@shared/core-utils'
|
||||
import { escapeHTML } from '@shared/core-utils/renderer'
|
||||
import { sha256 } from '@shared/extra-utils'
|
||||
|
@ -16,7 +17,6 @@ import { mdToOneLinePlainText } from '../helpers/markdown'
|
|||
import { CONFIG } from '../initializers/config'
|
||||
import {
|
||||
ACCEPT_HEADERS,
|
||||
ACTOR_IMAGES_SIZE,
|
||||
CUSTOM_HTML_TAG_COMMENTS,
|
||||
EMBED_SIZE,
|
||||
FILES_CONTENT_HASH,
|
||||
|
@ -29,6 +29,7 @@ import { VideoModel } from '../models/video/video'
|
|||
import { VideoChannelModel } from '../models/video/video-channel'
|
||||
import { VideoPlaylistModel } from '../models/video/video-playlist'
|
||||
import { MAccountActor, MChannelActor } from '../types/models'
|
||||
import { getBiggestActorImage } from './actor-image'
|
||||
import { ServerConfigManager } from './server-config-manager'
|
||||
|
||||
type Tags = {
|
||||
|
@ -273,10 +274,11 @@ class ClientHtml {
|
|||
const siteName = CONFIG.INSTANCE.NAME
|
||||
const title = entity.getDisplayName()
|
||||
|
||||
const avatar = getBiggestActorImage(entity.Actor.Avatars)
|
||||
const image = {
|
||||
url: entity.Actor.getAvatarUrl(),
|
||||
width: ACTOR_IMAGES_SIZE.AVATARS.width,
|
||||
height: ACTOR_IMAGES_SIZE.AVATARS.height
|
||||
url: ActorImageModel.getImageUrl(avatar),
|
||||
width: avatar?.width,
|
||||
height: avatar?.height
|
||||
}
|
||||
|
||||
const ogType = 'website'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'multer'
|
||||
import { queue } from 'async'
|
||||
import { remove } from 'fs-extra'
|
||||
import LRUCache from 'lru-cache'
|
||||
import { join } from 'path'
|
||||
import { ActorModel } from '@server/models/actor/actor'
|
||||
|
@ -13,7 +13,7 @@ import { CONFIG } from '../initializers/config'
|
|||
import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants'
|
||||
import { sequelizeTypescript } from '../initializers/database'
|
||||
import { MAccountDefault, MActor, MChannelDefault } from '../types/models'
|
||||
import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actors'
|
||||
import { deleteActorImages, updateActorImages } from './activitypub/actors'
|
||||
import { sendUpdateActor } from './activitypub/send'
|
||||
|
||||
function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) {
|
||||
|
@ -33,64 +33,69 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU
|
|||
}) as MActor
|
||||
}
|
||||
|
||||
async function updateLocalActorImageFile (
|
||||
async function updateLocalActorImageFiles (
|
||||
accountOrChannel: MAccountDefault | MChannelDefault,
|
||||
imagePhysicalFile: Express.Multer.File,
|
||||
type: ActorImageType
|
||||
) {
|
||||
const imageSize = type === ActorImageType.AVATAR
|
||||
? ACTOR_IMAGES_SIZE.AVATARS
|
||||
: ACTOR_IMAGES_SIZE.BANNERS
|
||||
const processImageSize = async (imageSize: { width: number, height: number }) => {
|
||||
const extension = getLowercaseExtension(imagePhysicalFile.filename)
|
||||
|
||||
const extension = getLowercaseExtension(imagePhysicalFile.filename)
|
||||
const imageName = buildUUID() + extension
|
||||
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
|
||||
await processImage(imagePhysicalFile.path, destination, imageSize, true)
|
||||
|
||||
const imageName = buildUUID() + extension
|
||||
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
|
||||
await processImage(imagePhysicalFile.path, destination, imageSize)
|
||||
return {
|
||||
imageName,
|
||||
imageSize
|
||||
}
|
||||
}
|
||||
|
||||
return retryTransactionWrapper(() => {
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
const actorImageInfo = {
|
||||
name: imageName,
|
||||
fileUrl: null,
|
||||
height: imageSize.height,
|
||||
width: imageSize.width,
|
||||
onDisk: true
|
||||
}
|
||||
const processedImages = await Promise.all(ACTOR_IMAGES_SIZE[type].map(processImageSize))
|
||||
await remove(imagePhysicalFile.path)
|
||||
|
||||
const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, type, actorImageInfo, t)
|
||||
await updatedActor.save({ transaction: t })
|
||||
return retryTransactionWrapper(() => sequelizeTypescript.transaction(async t => {
|
||||
const actorImagesInfo = processedImages.map(({ imageName, imageSize }) => ({
|
||||
name: imageName,
|
||||
fileUrl: null,
|
||||
height: imageSize.height,
|
||||
width: imageSize.width,
|
||||
onDisk: true
|
||||
}))
|
||||
|
||||
await sendUpdateActor(accountOrChannel, t)
|
||||
const updatedActor = await updateActorImages(accountOrChannel.Actor, type, actorImagesInfo, t)
|
||||
await updatedActor.save({ transaction: t })
|
||||
|
||||
return type === ActorImageType.AVATAR
|
||||
? updatedActor.Avatar
|
||||
: updatedActor.Banner
|
||||
})
|
||||
})
|
||||
await sendUpdateActor(accountOrChannel, t)
|
||||
|
||||
return type === ActorImageType.AVATAR
|
||||
? updatedActor.Avatars
|
||||
: updatedActor.Banners
|
||||
}))
|
||||
}
|
||||
|
||||
async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) {
|
||||
return retryTransactionWrapper(() => {
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t)
|
||||
const updatedActor = await deleteActorImages(accountOrChannel.Actor, type, t)
|
||||
await updatedActor.save({ transaction: t })
|
||||
|
||||
await sendUpdateActor(accountOrChannel, t)
|
||||
|
||||
return updatedActor.Avatar
|
||||
return updatedActor.Avatars
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType }
|
||||
type DownloadImageQueueTask = {
|
||||
fileUrl: string
|
||||
filename: string
|
||||
type: ActorImageType
|
||||
size: typeof ACTOR_IMAGES_SIZE[ActorImageType][0]
|
||||
}
|
||||
|
||||
const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
|
||||
const size = task.type === ActorImageType.AVATAR
|
||||
? ACTOR_IMAGES_SIZE.AVATARS
|
||||
: ACTOR_IMAGES_SIZE.BANNERS
|
||||
|
||||
downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size)
|
||||
downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, task.size)
|
||||
.then(() => cb())
|
||||
.catch(err => cb(err))
|
||||
}, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE)
|
||||
|
@ -110,7 +115,7 @@ const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.
|
|||
|
||||
export {
|
||||
actorImagePathUnsafeCache,
|
||||
updateLocalActorImageFile,
|
||||
updateLocalActorImageFiles,
|
||||
deleteLocalActorImageFile,
|
||||
pushActorImageProcessInQueue,
|
||||
buildActorInstance
|
||||
|
|
|
@ -77,7 +77,7 @@ export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MU
|
|||
userId: user.id,
|
||||
commentId: this.payload.id
|
||||
})
|
||||
notification.Comment = this.payload
|
||||
notification.VideoComment = this.payload
|
||||
|
||||
return notification
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ export class NewCommentForVideoOwner extends AbstractNotification <MCommentOwner
|
|||
userId: user.id,
|
||||
commentId: this.payload.id
|
||||
})
|
||||
notification.Comment = this.payload
|
||||
notification.VideoComment = this.payload
|
||||
|
||||
return notification
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses'
|
||||
import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { AbuseMessage } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
|
||||
import { getSort, throwIfNotValid } from '../utils'
|
||||
import { AbuseModel } from './abuse'
|
||||
import { FindOptions } from 'sequelize/dist'
|
||||
|
||||
@Table({
|
||||
tableName: 'abuseMessage',
|
||||
|
@ -62,21 +63,28 @@ export class AbuseMessageModel extends Model<Partial<AttributesOnly<AbuseMessage
|
|||
Abuse: AbuseModel
|
||||
|
||||
static listForApi (abuseId: number) {
|
||||
const options = {
|
||||
where: { abuseId },
|
||||
const getQuery = (forCount: boolean) => {
|
||||
const query: FindOptions = {
|
||||
where: { abuseId },
|
||||
order: getSort('createdAt')
|
||||
}
|
||||
|
||||
order: getSort('createdAt'),
|
||||
if (forCount !== true) {
|
||||
query.include = [
|
||||
{
|
||||
model: AccountModel.scope(AccountScopeNames.SUMMARY),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
include: [
|
||||
{
|
||||
model: AccountModel.scope(AccountScopeNames.SUMMARY),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
return query
|
||||
}
|
||||
|
||||
return AbuseMessageModel.findAndCountAll(options)
|
||||
.then(({ rows, count }) => ({ data: rows, total: count }))
|
||||
return Promise.all([
|
||||
AbuseMessageModel.count(getQuery(true)),
|
||||
AbuseMessageModel.findAll(getQuery(false))
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static loadByIdAndAbuseId (messageId: number, abuseId: number): Promise<MAbuseMessage> {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Op, QueryTypes } from 'sequelize'
|
||||
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { FindOptions, Op, QueryTypes } from 'sequelize'
|
||||
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { handlesToNameAndHost } from '@server/helpers/actors'
|
||||
import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
|
||||
import { MAccountBlocklist, MAccountBlocklistFormattable } from '@server/types/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { AccountBlock } from '../../../shared/models'
|
||||
import { ActorModel } from '../actor/actor'
|
||||
|
@ -9,27 +9,6 @@ import { ServerModel } from '../server/server'
|
|||
import { createSafeIn, getSort, searchAttribute } from '../utils'
|
||||
import { AccountModel } from './account'
|
||||
|
||||
enum ScopeNames {
|
||||
WITH_ACCOUNTS = 'WITH_ACCOUNTS'
|
||||
}
|
||||
|
||||
@Scopes(() => ({
|
||||
[ScopeNames.WITH_ACCOUNTS]: {
|
||||
include: [
|
||||
{
|
||||
model: AccountModel,
|
||||
required: true,
|
||||
as: 'ByAccount'
|
||||
},
|
||||
{
|
||||
model: AccountModel,
|
||||
required: true,
|
||||
as: 'BlockedAccount'
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
|
||||
@Table({
|
||||
tableName: 'accountBlocklist',
|
||||
indexes: [
|
||||
|
@ -123,33 +102,45 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB
|
|||
}) {
|
||||
const { start, count, sort, search, accountId } = parameters
|
||||
|
||||
const query = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getSort(sort)
|
||||
}
|
||||
const getQuery = (forCount: boolean) => {
|
||||
const query: FindOptions = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getSort(sort),
|
||||
where: { accountId }
|
||||
}
|
||||
|
||||
const where = {
|
||||
accountId
|
||||
}
|
||||
if (search) {
|
||||
Object.assign(query.where, {
|
||||
[Op.or]: [
|
||||
searchAttribute(search, '$BlockedAccount.name$'),
|
||||
searchAttribute(search, '$BlockedAccount.Actor.url$')
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (search) {
|
||||
Object.assign(where, {
|
||||
[Op.or]: [
|
||||
searchAttribute(search, '$BlockedAccount.name$'),
|
||||
searchAttribute(search, '$BlockedAccount.Actor.url$')
|
||||
if (forCount !== true) {
|
||||
query.include = [
|
||||
{
|
||||
model: AccountModel,
|
||||
required: true,
|
||||
as: 'ByAccount'
|
||||
},
|
||||
{
|
||||
model: AccountModel,
|
||||
required: true,
|
||||
as: 'BlockedAccount'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
Object.assign(query, { where })
|
||||
|
||||
return AccountBlocklistModel
|
||||
.scope([ ScopeNames.WITH_ACCOUNTS ])
|
||||
.findAndCountAll<MAccountBlocklistAccounts>(query)
|
||||
.then(({ rows, count }) => {
|
||||
return { total: count, data: rows }
|
||||
})
|
||||
return Promise.all([
|
||||
AccountBlocklistModel.count(getQuery(true)),
|
||||
AccountBlocklistModel.findAll(getQuery(false))
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static listHandlesBlockedBy (accountIds: number[]): Promise<string[]> {
|
||||
|
|
|
@ -121,29 +121,40 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV
|
|||
type?: string
|
||||
accountId: number
|
||||
}) {
|
||||
const query: FindOptions = {
|
||||
offset: options.start,
|
||||
limit: options.count,
|
||||
order: getSort(options.sort),
|
||||
where: {
|
||||
accountId: options.accountId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: VideoModel,
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
|
||||
required: true
|
||||
}
|
||||
]
|
||||
const getQuery = (forCount: boolean) => {
|
||||
const query: FindOptions = {
|
||||
offset: options.start,
|
||||
limit: options.count,
|
||||
order: getSort(options.sort),
|
||||
where: {
|
||||
accountId: options.accountId
|
||||
}
|
||||
]
|
||||
}
|
||||
if (options.type) query.where['type'] = options.type
|
||||
}
|
||||
|
||||
return AccountVideoRateModel.findAndCountAll(query)
|
||||
if (options.type) query.where['type'] = options.type
|
||||
|
||||
if (forCount !== true) {
|
||||
query.include = [
|
||||
{
|
||||
model: VideoModel,
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
AccountVideoRateModel.count(getQuery(true)),
|
||||
AccountVideoRateModel.findAll(getQuery(false))
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static listRemoteRateUrlsOfLocalVideos () {
|
||||
|
@ -232,7 +243,10 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV
|
|||
]
|
||||
}
|
||||
|
||||
return AccountVideoRateModel.findAndCountAll<MAccountVideoRateAccountUrl>(query)
|
||||
return Promise.all([
|
||||
AccountVideoRateModel.count(query),
|
||||
AccountVideoRateModel.findAll<MAccountVideoRateAccountUrl>(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) {
|
||||
|
|
|
@ -54,6 +54,7 @@ export type SummaryOptions = {
|
|||
whereActor?: WhereOptions
|
||||
whereServer?: WhereOptions
|
||||
withAccountBlockerIds?: number[]
|
||||
forCount?: boolean
|
||||
}
|
||||
|
||||
@DefaultScope(() => ({
|
||||
|
@ -73,22 +74,24 @@ export type SummaryOptions = {
|
|||
where: options.whereServer
|
||||
}
|
||||
|
||||
const queryInclude: Includeable[] = [
|
||||
{
|
||||
attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
|
||||
model: ActorModel.unscoped(),
|
||||
required: options.actorRequired ?? true,
|
||||
where: options.whereActor,
|
||||
include: [
|
||||
serverInclude,
|
||||
const actorInclude: Includeable = {
|
||||
attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ],
|
||||
model: ActorModel.unscoped(),
|
||||
required: options.actorRequired ?? true,
|
||||
where: options.whereActor,
|
||||
include: [ serverInclude ]
|
||||
}
|
||||
|
||||
{
|
||||
model: ActorImageModel.unscoped(),
|
||||
as: 'Avatar',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
if (options.forCount !== true) {
|
||||
actorInclude.include.push({
|
||||
model: ActorImageModel,
|
||||
as: 'Avatars',
|
||||
required: false
|
||||
})
|
||||
}
|
||||
|
||||
const queryInclude: Includeable[] = [
|
||||
actorInclude
|
||||
]
|
||||
|
||||
const query: FindOptions = {
|
||||
|
@ -349,13 +352,10 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
|
|||
order: getSort(sort)
|
||||
}
|
||||
|
||||
return AccountModel.findAndCountAll(query)
|
||||
.then(({ rows, count }) => {
|
||||
return {
|
||||
data: rows,
|
||||
total: count
|
||||
}
|
||||
})
|
||||
return Promise.all([
|
||||
AccountModel.count(),
|
||||
AccountModel.findAll(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static loadAccountIdFromVideo (videoId: number): Promise<MAccount> {
|
||||
|
@ -407,16 +407,15 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
|
|||
}
|
||||
|
||||
toFormattedJSON (this: MAccountFormattable): Account {
|
||||
const actor = this.Actor.toFormattedJSON()
|
||||
const account = {
|
||||
return {
|
||||
...this.Actor.toFormattedJSON(),
|
||||
|
||||
id: this.id,
|
||||
displayName: this.getDisplayName(),
|
||||
description: this.description,
|
||||
updatedAt: this.updatedAt,
|
||||
userId: this.userId ? this.userId : undefined
|
||||
userId: this.userId ?? undefined
|
||||
}
|
||||
|
||||
return Object.assign(actor, account)
|
||||
}
|
||||
|
||||
toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary {
|
||||
|
@ -424,10 +423,14 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
|
|||
|
||||
return {
|
||||
id: this.id,
|
||||
name: actor.name,
|
||||
displayName: this.getDisplayName(),
|
||||
|
||||
name: actor.name,
|
||||
url: actor.url,
|
||||
host: actor.host,
|
||||
avatars: actor.avatars,
|
||||
|
||||
// TODO: remove, deprecated in 4.2
|
||||
avatar: actor.avatar
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { difference, values } from 'lodash'
|
||||
import { IncludeOptions, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize'
|
||||
import { Includeable, IncludeOptions, literal, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize'
|
||||
import {
|
||||
AfterCreate,
|
||||
AfterDestroy,
|
||||
|
@ -30,12 +30,12 @@ import {
|
|||
MActorFollowFormattable,
|
||||
MActorFollowSubscriptions
|
||||
} from '@server/types/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { ActivityPubActorType } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { FollowState } from '../../../shared/models/actors'
|
||||
import { ActorFollow } from '../../../shared/models/actors/follow.model'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
|
||||
import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME, SORTABLE_COLUMNS } from '../../initializers/constants'
|
||||
import { AccountModel } from '../account/account'
|
||||
import { ServerModel } from '../server/server'
|
||||
import { doesExist } from '../shared/query'
|
||||
|
@ -375,43 +375,46 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
|||
Object.assign(followingWhere, { type: actorType })
|
||||
}
|
||||
|
||||
const query = {
|
||||
distinct: true,
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getFollowsSort(sort),
|
||||
where: followWhere,
|
||||
include: [
|
||||
{
|
||||
model: ActorModel,
|
||||
required: true,
|
||||
as: 'ActorFollower',
|
||||
where: {
|
||||
id
|
||||
}
|
||||
},
|
||||
{
|
||||
model: ActorModel,
|
||||
as: 'ActorFollowing',
|
||||
required: true,
|
||||
where: followingWhere,
|
||||
include: [
|
||||
{
|
||||
model: ServerModel,
|
||||
required: true
|
||||
const getQuery = (forCount: boolean) => {
|
||||
const actorModel = forCount
|
||||
? ActorModel.unscoped()
|
||||
: ActorModel
|
||||
|
||||
return {
|
||||
distinct: true,
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getFollowsSort(sort),
|
||||
where: followWhere,
|
||||
include: [
|
||||
{
|
||||
model: actorModel,
|
||||
required: true,
|
||||
as: 'ActorFollower',
|
||||
where: {
|
||||
id
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: actorModel,
|
||||
as: 'ActorFollowing',
|
||||
required: true,
|
||||
where: followingWhere,
|
||||
include: [
|
||||
{
|
||||
model: ServerModel,
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
|
||||
.then(({ rows, count }) => {
|
||||
return {
|
||||
data: rows,
|
||||
total: count
|
||||
}
|
||||
})
|
||||
return Promise.all([
|
||||
ActorFollowModel.count(getQuery(true)),
|
||||
ActorFollowModel.findAll<MActorFollowActorsDefault>(getQuery(false))
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static listFollowersForApi (options: {
|
||||
|
@ -429,11 +432,17 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
|||
const followerWhere: WhereOptions = {}
|
||||
|
||||
if (search) {
|
||||
Object.assign(followWhere, {
|
||||
[Op.or]: [
|
||||
searchAttribute(search, '$ActorFollower.preferredUsername$'),
|
||||
searchAttribute(search, '$ActorFollower.Server.host$')
|
||||
]
|
||||
const escapedSearch = ActorFollowModel.sequelize.escape('%' + search + '%')
|
||||
|
||||
Object.assign(followerWhere, {
|
||||
id: {
|
||||
[Op.in]: literal(
|
||||
`(` +
|
||||
`SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" ` +
|
||||
`WHERE "preferredUsername" ILIKE ${escapedSearch} OR "host" ILIKE ${escapedSearch}` +
|
||||
`)`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -441,39 +450,43 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
|||
Object.assign(followerWhere, { type: actorType })
|
||||
}
|
||||
|
||||
const query = {
|
||||
distinct: true,
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getFollowsSort(sort),
|
||||
where: followWhere,
|
||||
include: [
|
||||
{
|
||||
model: ActorModel,
|
||||
required: true,
|
||||
as: 'ActorFollower',
|
||||
where: followerWhere
|
||||
},
|
||||
{
|
||||
model: ActorModel,
|
||||
as: 'ActorFollowing',
|
||||
required: true,
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: actorIds
|
||||
const getQuery = (forCount: boolean) => {
|
||||
const actorModel = forCount
|
||||
? ActorModel.unscoped()
|
||||
: ActorModel
|
||||
|
||||
return {
|
||||
distinct: true,
|
||||
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getFollowsSort(sort),
|
||||
where: followWhere,
|
||||
include: [
|
||||
{
|
||||
model: actorModel,
|
||||
required: true,
|
||||
as: 'ActorFollower',
|
||||
where: followerWhere
|
||||
},
|
||||
{
|
||||
model: actorModel,
|
||||
as: 'ActorFollowing',
|
||||
required: true,
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: actorIds
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
|
||||
.then(({ rows, count }) => {
|
||||
return {
|
||||
data: rows,
|
||||
total: count
|
||||
}
|
||||
})
|
||||
return Promise.all([
|
||||
ActorFollowModel.count(getQuery(true)),
|
||||
ActorFollowModel.findAll<MActorFollowActorsDefault>(getQuery(false))
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static listSubscriptionsForApi (options: {
|
||||
|
@ -497,58 +510,68 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
|
|||
})
|
||||
}
|
||||
|
||||
const query = {
|
||||
attributes: [],
|
||||
distinct: true,
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getSort(sort),
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'id' ],
|
||||
model: ActorModel.unscoped(),
|
||||
as: 'ActorFollowing',
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
model: VideoChannelModel.unscoped(),
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
attributes: {
|
||||
exclude: unusedActorAttributesForAPI
|
||||
},
|
||||
model: ActorModel,
|
||||
required: true
|
||||
const getQuery = (forCount: boolean) => {
|
||||
let channelInclude: Includeable[] = []
|
||||
|
||||
if (forCount !== true) {
|
||||
channelInclude = [
|
||||
{
|
||||
attributes: {
|
||||
exclude: unusedActorAttributesForAPI
|
||||
},
|
||||
model: ActorModel,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
model: AccountModel.unscoped(),
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
attributes: {
|
||||
exclude: unusedActorAttributesForAPI
|
||||
},
|
||||
{
|
||||
model: AccountModel.unscoped(),
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
attributes: {
|
||||
exclude: unusedActorAttributesForAPI
|
||||
},
|
||||
model: ActorModel,
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
model: ActorModel,
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return {
|
||||
attributes: forCount === true
|
||||
? []
|
||||
: SORTABLE_COLUMNS.USER_SUBSCRIPTIONS,
|
||||
distinct: true,
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getSort(sort),
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'id' ],
|
||||
model: ActorModel.unscoped(),
|
||||
as: 'ActorFollowing',
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
model: VideoChannelModel.unscoped(),
|
||||
required: true,
|
||||
include: channelInclude
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return ActorFollowModel.findAndCountAll<MActorFollowSubscriptions>(query)
|
||||
.then(({ rows, count }) => {
|
||||
return {
|
||||
data: rows.map(r => r.ActorFollowing.VideoChannel),
|
||||
total: count
|
||||
}
|
||||
})
|
||||
return Promise.all([
|
||||
ActorFollowModel.count(getQuery(true)),
|
||||
ActorFollowModel.findAll<MActorFollowSubscriptions>(getQuery(false))
|
||||
]).then(([ total, rows ]) => ({
|
||||
total,
|
||||
data: rows.map(r => r.ActorFollowing.VideoChannel)
|
||||
}))
|
||||
}
|
||||
|
||||
static async keepUnfollowedInstance (hosts: string[]) {
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
import { remove } from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { MActorImageFormattable } from '@server/types/models'
|
||||
import {
|
||||
AfterDestroy,
|
||||
AllowNull,
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
Default,
|
||||
ForeignKey,
|
||||
Is,
|
||||
Model,
|
||||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { MActorImage, MActorImageFormattable } from '@server/types/models'
|
||||
import { getLowercaseExtension } from '@shared/core-utils'
|
||||
import { ActivityIconObject, ActorImageType } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { ActorImageType } from '@shared/models'
|
||||
import { ActorImage } from '../../../shared/models/actors/actor-image.model'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { CONFIG } from '../../initializers/config'
|
||||
import { LAZY_STATIC_PATHS } from '../../initializers/constants'
|
||||
import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants'
|
||||
import { throwIfNotValid } from '../utils'
|
||||
import { ActorModel } from './actor'
|
||||
|
||||
@Table({
|
||||
tableName: 'actorImage',
|
||||
|
@ -17,6 +31,10 @@ import { throwIfNotValid } from '../utils'
|
|||
{
|
||||
fields: [ 'filename' ],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: [ 'actorId', 'type', 'width' ],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -55,6 +73,18 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode
|
|||
@UpdatedAt
|
||||
updatedAt: Date
|
||||
|
||||
@ForeignKey(() => ActorModel)
|
||||
@Column
|
||||
actorId: number
|
||||
|
||||
@BelongsTo(() => ActorModel, {
|
||||
foreignKey: {
|
||||
allowNull: false
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
Actor: ActorModel
|
||||
|
||||
@AfterDestroy
|
||||
static removeFilesAndSendDelete (instance: ActorImageModel) {
|
||||
logger.info('Removing actor image file %s.', instance.filename)
|
||||
|
@ -74,20 +104,41 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode
|
|||
return ActorImageModel.findOne(query)
|
||||
}
|
||||
|
||||
static getImageUrl (image: MActorImage) {
|
||||
if (!image) return undefined
|
||||
|
||||
return WEBSERVER.URL + image.getStaticPath()
|
||||
}
|
||||
|
||||
toFormattedJSON (this: MActorImageFormattable): ActorImage {
|
||||
return {
|
||||
width: this.width,
|
||||
path: this.getStaticPath(),
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
getStaticPath () {
|
||||
if (this.type === ActorImageType.AVATAR) {
|
||||
return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
|
||||
}
|
||||
toActivityPubObject (): ActivityIconObject {
|
||||
const extension = getLowercaseExtension(this.filename)
|
||||
|
||||
return join(LAZY_STATIC_PATHS.BANNERS, this.filename)
|
||||
return {
|
||||
type: 'Image',
|
||||
mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
|
||||
height: this.height,
|
||||
width: this.width,
|
||||
url: ActorImageModel.getImageUrl(this)
|
||||
}
|
||||
}
|
||||
|
||||
getStaticPath () {
|
||||
switch (this.type) {
|
||||
case ActorImageType.AVATAR:
|
||||
return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
|
||||
|
||||
case ActorImageType.BANNER:
|
||||
return join(LAZY_STATIC_PATHS.BANNERS, this.filename)
|
||||
}
|
||||
}
|
||||
|
||||
getPath () {
|
||||
|
|
|
@ -16,11 +16,11 @@ import {
|
|||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { getBiggestActorImage } from '@server/lib/actor-image'
|
||||
import { ModelCache } from '@server/models/model-cache'
|
||||
import { getLowercaseExtension } from '@shared/core-utils'
|
||||
import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
|
||||
import { ActorImage } from '../../../shared/models/actors/actor-image.model'
|
||||
import { activityPubContextify } from '../../helpers/activitypub'
|
||||
import {
|
||||
isActorFollowersCountValid,
|
||||
|
@ -81,7 +81,7 @@ export const unusedActorAttributesForAPI = [
|
|||
},
|
||||
{
|
||||
model: ActorImageModel,
|
||||
as: 'Avatar',
|
||||
as: 'Avatars',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
|
@ -109,12 +109,12 @@ export const unusedActorAttributesForAPI = [
|
|||
},
|
||||
{
|
||||
model: ActorImageModel,
|
||||
as: 'Avatar',
|
||||
as: 'Avatars',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: ActorImageModel,
|
||||
as: 'Banner',
|
||||
as: 'Banners',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
|
@ -152,9 +152,6 @@ export const unusedActorAttributesForAPI = [
|
|||
{
|
||||
fields: [ 'serverId' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'avatarId' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'followersUrl' ]
|
||||
}
|
||||
|
@ -231,35 +228,31 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
|
|||
@UpdatedAt
|
||||
updatedAt: Date
|
||||
|
||||
@ForeignKey(() => ActorImageModel)
|
||||
@Column
|
||||
avatarId: number
|
||||
|
||||
@ForeignKey(() => ActorImageModel)
|
||||
@Column
|
||||
bannerId: number
|
||||
|
||||
@BelongsTo(() => ActorImageModel, {
|
||||
@HasMany(() => ActorImageModel, {
|
||||
as: 'Avatars',
|
||||
onDelete: 'cascade',
|
||||
hooks: true,
|
||||
foreignKey: {
|
||||
name: 'avatarId',
|
||||
allowNull: true
|
||||
allowNull: false
|
||||
},
|
||||
as: 'Avatar',
|
||||
onDelete: 'set null',
|
||||
hooks: true
|
||||
scope: {
|
||||
type: ActorImageType.AVATAR
|
||||
}
|
||||
})
|
||||
Avatar: ActorImageModel
|
||||
Avatars: ActorImageModel[]
|
||||
|
||||
@BelongsTo(() => ActorImageModel, {
|
||||
@HasMany(() => ActorImageModel, {
|
||||
as: 'Banners',
|
||||
onDelete: 'cascade',
|
||||
hooks: true,
|
||||
foreignKey: {
|
||||
name: 'bannerId',
|
||||
allowNull: true
|
||||
allowNull: false
|
||||
},
|
||||
as: 'Banner',
|
||||
onDelete: 'set null',
|
||||
hooks: true
|
||||
scope: {
|
||||
type: ActorImageType.BANNER
|
||||
}
|
||||
})
|
||||
Banner: ActorImageModel
|
||||
Banners: ActorImageModel[]
|
||||
|
||||
@HasMany(() => ActorFollowModel, {
|
||||
foreignKey: {
|
||||
|
@ -386,8 +379,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
|
|||
transaction
|
||||
}
|
||||
|
||||
return ActorModel.scope(ScopeNames.FULL)
|
||||
.findOne(query)
|
||||
return ActorModel.scope(ScopeNames.FULL).findOne(query)
|
||||
}
|
||||
|
||||
return ModelCache.Instance.doCache({
|
||||
|
@ -410,8 +402,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
|
|||
transaction
|
||||
}
|
||||
|
||||
return ActorModel.unscoped()
|
||||
.findOne(query)
|
||||
return ActorModel.unscoped().findOne(query)
|
||||
}
|
||||
|
||||
return ModelCache.Instance.doCache({
|
||||
|
@ -532,55 +523,50 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
|
|||
}
|
||||
|
||||
toFormattedSummaryJSON (this: MActorSummaryFormattable) {
|
||||
let avatar: ActorImage = null
|
||||
if (this.Avatar) {
|
||||
avatar = this.Avatar.toFormattedJSON()
|
||||
}
|
||||
|
||||
return {
|
||||
url: this.url,
|
||||
name: this.preferredUsername,
|
||||
host: this.getHost(),
|
||||
avatar
|
||||
avatars: (this.Avatars || []).map(a => a.toFormattedJSON()),
|
||||
|
||||
// TODO: remove, deprecated in 4.2
|
||||
avatar: this.hasImage(ActorImageType.AVATAR)
|
||||
? this.Avatars[0].toFormattedJSON()
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
toFormattedJSON (this: MActorFormattable) {
|
||||
const base = this.toFormattedSummaryJSON()
|
||||
return {
|
||||
...this.toFormattedSummaryJSON(),
|
||||
|
||||
let banner: ActorImage = null
|
||||
if (this.Banner) {
|
||||
banner = this.Banner.toFormattedJSON()
|
||||
}
|
||||
|
||||
return Object.assign(base, {
|
||||
id: this.id,
|
||||
hostRedundancyAllowed: this.getRedundancyAllowed(),
|
||||
followingCount: this.followingCount,
|
||||
followersCount: this.followersCount,
|
||||
banner,
|
||||
createdAt: this.getCreatedAt()
|
||||
})
|
||||
createdAt: this.getCreatedAt(),
|
||||
|
||||
banners: (this.Banners || []).map(b => b.toFormattedJSON()),
|
||||
|
||||
// TODO: remove, deprecated in 4.2
|
||||
banner: this.hasImage(ActorImageType.BANNER)
|
||||
? this.Banners[0].toFormattedJSON()
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
|
||||
let icon: ActivityIconObject
|
||||
let icons: ActivityIconObject[]
|
||||
let image: ActivityIconObject
|
||||
|
||||
if (this.avatarId) {
|
||||
const extension = getLowercaseExtension(this.Avatar.filename)
|
||||
|
||||
icon = {
|
||||
type: 'Image',
|
||||
mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
|
||||
height: this.Avatar.height,
|
||||
width: this.Avatar.width,
|
||||
url: this.getAvatarUrl()
|
||||
}
|
||||
if (this.hasImage(ActorImageType.AVATAR)) {
|
||||
icon = getBiggestActorImage(this.Avatars).toActivityPubObject()
|
||||
icons = this.Avatars.map(a => a.toActivityPubObject())
|
||||
}
|
||||
|
||||
if (this.bannerId) {
|
||||
const banner = (this as MActorAPChannel).Banner
|
||||
if (this.hasImage(ActorImageType.BANNER)) {
|
||||
const banner = getBiggestActorImage((this as MActorAPChannel).Banners)
|
||||
const extension = getLowercaseExtension(banner.filename)
|
||||
|
||||
image = {
|
||||
|
@ -588,7 +574,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
|
|||
mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
|
||||
height: banner.height,
|
||||
width: banner.width,
|
||||
url: this.getBannerUrl()
|
||||
url: ActorImageModel.getImageUrl(banner)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -612,7 +598,10 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
|
|||
publicKeyPem: this.publicKey
|
||||
},
|
||||
published: this.getCreatedAt().toISOString(),
|
||||
|
||||
icon,
|
||||
icons,
|
||||
|
||||
image
|
||||
}
|
||||
|
||||
|
@ -677,16 +666,12 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
|
|||
return this.Server ? this.Server.redundancyAllowed : false
|
||||
}
|
||||
|
||||
getAvatarUrl () {
|
||||
if (!this.avatarId) return undefined
|
||||
hasImage (type: ActorImageType) {
|
||||
const images = type === ActorImageType.AVATAR
|
||||
? this.Avatars
|
||||
: this.Banners
|
||||
|
||||
return WEBSERVER.URL + this.Avatar.getStaticPath()
|
||||
}
|
||||
|
||||
getBannerUrl () {
|
||||
if (!this.bannerId) return undefined
|
||||
|
||||
return WEBSERVER.URL + this.Banner.getStaticPath()
|
||||
return Array.isArray(images) && images.length !== 0
|
||||
}
|
||||
|
||||
isOutdated () {
|
||||
|
|
|
@ -239,11 +239,10 @@ export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> {
|
|||
|
||||
if (options.pluginType) query.where['type'] = options.pluginType
|
||||
|
||||
return PluginModel
|
||||
.findAndCountAll<MPlugin>(query)
|
||||
.then(({ rows, count }) => {
|
||||
return { total: count, data: rows }
|
||||
})
|
||||
return Promise.all([
|
||||
PluginModel.count(query),
|
||||
PluginModel.findAll<MPlugin>(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static listInstalled (): Promise<MPlugin[]> {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Op, QueryTypes } from 'sequelize'
|
||||
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { ServerBlock } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { AccountModel } from '../account/account'
|
||||
import { createSafeIn, getSort, searchAttribute } from '../utils'
|
||||
import { ServerModel } from './server'
|
||||
|
@ -169,16 +169,15 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo
|
|||
order: getSort(sort),
|
||||
where: {
|
||||
accountId,
|
||||
|
||||
...searchAttribute(search, '$BlockedServer.host$')
|
||||
}
|
||||
}
|
||||
|
||||
return ServerBlocklistModel
|
||||
.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ])
|
||||
.findAndCountAll<MServerBlocklistAccountServer>(query)
|
||||
.then(({ rows, count }) => {
|
||||
return { total: count, data: rows }
|
||||
})
|
||||
return Promise.all([
|
||||
ServerBlocklistModel.scope(ScopeNames.WITH_SERVER).count(query),
|
||||
ServerBlocklistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ]).findAll<MServerBlocklistAccountServer>(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
toFormattedJSON (this: MServerBlocklistFormattable): ServerBlock {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './model-builder'
|
||||
export * from './query'
|
||||
export * from './update'
|
||||
|
|
101
server/models/shared/model-builder.ts
Normal file
101
server/models/shared/model-builder.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { isPlainObject } from 'lodash'
|
||||
import { Model as SequelizeModel, Sequelize } from 'sequelize'
|
||||
import { logger } from '@server/helpers/logger'
|
||||
|
||||
export class ModelBuilder <T extends SequelizeModel> {
|
||||
private readonly modelRegistry = new Map<string, T>()
|
||||
|
||||
constructor (private readonly sequelize: Sequelize) {
|
||||
|
||||
}
|
||||
|
||||
createModels (jsonArray: any[], baseModelName: string): T[] {
|
||||
const result: T[] = []
|
||||
|
||||
for (const json of jsonArray) {
|
||||
const { created, model } = this.createModel(json, baseModelName, json.id + '.' + baseModelName)
|
||||
|
||||
if (created) result.push(model)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private createModel (json: any, modelName: string, keyPath: string) {
|
||||
if (!json.id) return { created: false, model: null }
|
||||
|
||||
const { created, model } = this.createOrFindModel(json, modelName, keyPath)
|
||||
|
||||
for (const key of Object.keys(json)) {
|
||||
const value = json[key]
|
||||
if (!value) continue
|
||||
|
||||
// Child model
|
||||
if (isPlainObject(value)) {
|
||||
const { created, model: subModel } = this.createModel(value, key, keyPath + '.' + json.id + '.' + key)
|
||||
if (!created || !subModel) continue
|
||||
|
||||
const Model = this.findModelBuilder(modelName)
|
||||
const association = Model.associations[key]
|
||||
|
||||
if (!association) {
|
||||
logger.error('Cannot find association %s of model %s', key, modelName, { associations: Object.keys(Model.associations) })
|
||||
continue
|
||||
}
|
||||
|
||||
if (association.isMultiAssociation) {
|
||||
if (!Array.isArray(model[key])) model[key] = []
|
||||
|
||||
model[key].push(subModel)
|
||||
} else {
|
||||
model[key] = subModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { created, model }
|
||||
}
|
||||
|
||||
private createOrFindModel (json: any, modelName: string, keyPath: string) {
|
||||
const registryKey = this.getModelRegistryKey(json, keyPath)
|
||||
if (this.modelRegistry.has(registryKey)) {
|
||||
return {
|
||||
created: false,
|
||||
model: this.modelRegistry.get(registryKey)
|
||||
}
|
||||
}
|
||||
|
||||
const Model = this.findModelBuilder(modelName)
|
||||
|
||||
if (!Model) {
|
||||
logger.error(
|
||||
'Cannot build model %s that does not exist', this.buildSequelizeModelName(modelName),
|
||||
{ existing: this.sequelize.modelManager.all.map(m => m.name) }
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// FIXME: typings
|
||||
const model = new (Model as any)(json)
|
||||
this.modelRegistry.set(registryKey, model)
|
||||
|
||||
return { created: true, model }
|
||||
}
|
||||
|
||||
private findModelBuilder (modelName: string) {
|
||||
return this.sequelize.modelManager.getModel(this.buildSequelizeModelName(modelName))
|
||||
}
|
||||
|
||||
private buildSequelizeModelName (modelName: string) {
|
||||
if (modelName === 'Avatars') return 'ActorImageModel'
|
||||
if (modelName === 'ActorFollowing') return 'ActorModel'
|
||||
if (modelName === 'ActorFollower') return 'ActorModel'
|
||||
if (modelName === 'FlaggedAccount') return 'AccountModel'
|
||||
|
||||
return modelName + 'Model'
|
||||
}
|
||||
|
||||
private getModelRegistryKey (json: any, keyPath: string) {
|
||||
return keyPath + json.id
|
||||
}
|
||||
}
|
269
server/models/user/sql/user-notitication-list-query-builder.ts
Normal file
269
server/models/user/sql/user-notitication-list-query-builder.ts
Normal file
|
@ -0,0 +1,269 @@
|
|||
import { QueryTypes, Sequelize } from 'sequelize'
|
||||
import { ModelBuilder } from '@server/models/shared'
|
||||
import { getSort } from '@server/models/utils'
|
||||
import { UserNotificationModelForApi } from '@server/types/models'
|
||||
import { ActorImageType } from '@shared/models'
|
||||
|
||||
export interface ListNotificationsOptions {
|
||||
userId: number
|
||||
unread?: boolean
|
||||
sort: string
|
||||
offset: number
|
||||
limit: number
|
||||
sequelize: Sequelize
|
||||
}
|
||||
|
||||
export class UserNotificationListQueryBuilder {
|
||||
private innerQuery: string
|
||||
private replacements: any = {}
|
||||
private query: string
|
||||
|
||||
constructor (private readonly options: ListNotificationsOptions) {
|
||||
|
||||
}
|
||||
|
||||
async listNotifications () {
|
||||
this.buildQuery()
|
||||
|
||||
const results = await this.options.sequelize.query(this.query, {
|
||||
replacements: this.replacements,
|
||||
type: QueryTypes.SELECT,
|
||||
nest: true
|
||||
})
|
||||
|
||||
const modelBuilder = new ModelBuilder<UserNotificationModelForApi>(this.options.sequelize)
|
||||
|
||||
return modelBuilder.createModels(results, 'UserNotification')
|
||||
}
|
||||
|
||||
private buildInnerQuery () {
|
||||
this.innerQuery = `SELECT * FROM "userNotification" AS "UserNotificationModel" ` +
|
||||
`${this.getWhere()} ` +
|
||||
`${this.getOrder()} ` +
|
||||
`LIMIT :limit OFFSET :offset `
|
||||
|
||||
this.replacements.limit = this.options.limit
|
||||
this.replacements.offset = this.options.offset
|
||||
}
|
||||
|
||||
private buildQuery () {
|
||||
this.buildInnerQuery()
|
||||
|
||||
this.query = `
|
||||
${this.getSelect()}
|
||||
FROM (${this.innerQuery}) "UserNotificationModel"
|
||||
${this.getJoins()}
|
||||
${this.getOrder()}`
|
||||
}
|
||||
|
||||
private getWhere () {
|
||||
let base = '"UserNotificationModel"."userId" = :userId '
|
||||
this.replacements.userId = this.options.userId
|
||||
|
||||
if (this.options.unread === true) {
|
||||
base += 'AND "UserNotificationModel"."read" IS FALSE '
|
||||
} else if (this.options.unread === false) {
|
||||
base += 'AND "UserNotificationModel"."read" IS TRUE '
|
||||
}
|
||||
|
||||
return `WHERE ${base}`
|
||||
}
|
||||
|
||||
private getOrder () {
|
||||
const orders = getSort(this.options.sort)
|
||||
|
||||
return 'ORDER BY ' + orders.map(o => `"UserNotificationModel"."${o[0]}" ${o[1]}`).join(', ')
|
||||
}
|
||||
|
||||
private getSelect () {
|
||||
return `SELECT
|
||||
"UserNotificationModel"."id",
|
||||
"UserNotificationModel"."type",
|
||||
"UserNotificationModel"."read",
|
||||
"UserNotificationModel"."createdAt",
|
||||
"UserNotificationModel"."updatedAt",
|
||||
"Video"."id" AS "Video.id",
|
||||
"Video"."uuid" AS "Video.uuid",
|
||||
"Video"."name" AS "Video.name",
|
||||
"Video->VideoChannel"."id" AS "Video.VideoChannel.id",
|
||||
"Video->VideoChannel"."name" AS "Video.VideoChannel.name",
|
||||
"Video->VideoChannel->Actor"."id" AS "Video.VideoChannel.Actor.id",
|
||||
"Video->VideoChannel->Actor"."preferredUsername" AS "Video.VideoChannel.Actor.preferredUsername",
|
||||
"Video->VideoChannel->Actor->Avatars"."id" AS "Video.VideoChannel.Actor.Avatars.id",
|
||||
"Video->VideoChannel->Actor->Avatars"."width" AS "Video.VideoChannel.Actor.Avatars.width",
|
||||
"Video->VideoChannel->Actor->Avatars"."filename" AS "Video.VideoChannel.Actor.Avatars.filename",
|
||||
"Video->VideoChannel->Actor->Server"."id" AS "Video.VideoChannel.Actor.Server.id",
|
||||
"Video->VideoChannel->Actor->Server"."host" AS "Video.VideoChannel.Actor.Server.host",
|
||||
"VideoComment"."id" AS "VideoComment.id",
|
||||
"VideoComment"."originCommentId" AS "VideoComment.originCommentId",
|
||||
"VideoComment->Account"."id" AS "VideoComment.Account.id",
|
||||
"VideoComment->Account"."name" AS "VideoComment.Account.name",
|
||||
"VideoComment->Account->Actor"."id" AS "VideoComment.Account.Actor.id",
|
||||
"VideoComment->Account->Actor"."preferredUsername" AS "VideoComment.Account.Actor.preferredUsername",
|
||||
"VideoComment->Account->Actor->Avatars"."id" AS "VideoComment.Account.Actor.Avatars.id",
|
||||
"VideoComment->Account->Actor->Avatars"."width" AS "VideoComment.Account.Actor.Avatars.width",
|
||||
"VideoComment->Account->Actor->Avatars"."filename" AS "VideoComment.Account.Actor.Avatars.filename",
|
||||
"VideoComment->Account->Actor->Server"."id" AS "VideoComment.Account.Actor.Server.id",
|
||||
"VideoComment->Account->Actor->Server"."host" AS "VideoComment.Account.Actor.Server.host",
|
||||
"VideoComment->Video"."id" AS "VideoComment.Video.id",
|
||||
"VideoComment->Video"."uuid" AS "VideoComment.Video.uuid",
|
||||
"VideoComment->Video"."name" AS "VideoComment.Video.name",
|
||||
"Abuse"."id" AS "Abuse.id",
|
||||
"Abuse"."state" AS "Abuse.state",
|
||||
"Abuse->VideoAbuse"."id" AS "Abuse.VideoAbuse.id",
|
||||
"Abuse->VideoAbuse->Video"."id" AS "Abuse.VideoAbuse.Video.id",
|
||||
"Abuse->VideoAbuse->Video"."uuid" AS "Abuse.VideoAbuse.Video.uuid",
|
||||
"Abuse->VideoAbuse->Video"."name" AS "Abuse.VideoAbuse.Video.name",
|
||||
"Abuse->VideoCommentAbuse"."id" AS "Abuse.VideoCommentAbuse.id",
|
||||
"Abuse->VideoCommentAbuse->VideoComment"."id" AS "Abuse.VideoCommentAbuse.VideoComment.id",
|
||||
"Abuse->VideoCommentAbuse->VideoComment"."originCommentId" AS "Abuse.VideoCommentAbuse.VideoComment.originCommentId",
|
||||
"Abuse->VideoCommentAbuse->VideoComment->Video"."id" AS "Abuse.VideoCommentAbuse.VideoComment.Video.id",
|
||||
"Abuse->VideoCommentAbuse->VideoComment->Video"."name" AS "Abuse.VideoCommentAbuse.VideoComment.Video.name",
|
||||
"Abuse->VideoCommentAbuse->VideoComment->Video"."uuid" AS "Abuse.VideoCommentAbuse.VideoComment.Video.uuid",
|
||||
"Abuse->FlaggedAccount"."id" AS "Abuse.FlaggedAccount.id",
|
||||
"Abuse->FlaggedAccount"."name" AS "Abuse.FlaggedAccount.name",
|
||||
"Abuse->FlaggedAccount"."description" AS "Abuse.FlaggedAccount.description",
|
||||
"Abuse->FlaggedAccount"."actorId" AS "Abuse.FlaggedAccount.actorId",
|
||||
"Abuse->FlaggedAccount"."userId" AS "Abuse.FlaggedAccount.userId",
|
||||
"Abuse->FlaggedAccount"."applicationId" AS "Abuse.FlaggedAccount.applicationId",
|
||||
"Abuse->FlaggedAccount"."createdAt" AS "Abuse.FlaggedAccount.createdAt",
|
||||
"Abuse->FlaggedAccount"."updatedAt" AS "Abuse.FlaggedAccount.updatedAt",
|
||||
"Abuse->FlaggedAccount->Actor"."id" AS "Abuse.FlaggedAccount.Actor.id",
|
||||
"Abuse->FlaggedAccount->Actor"."preferredUsername" AS "Abuse.FlaggedAccount.Actor.preferredUsername",
|
||||
"Abuse->FlaggedAccount->Actor->Avatars"."id" AS "Abuse.FlaggedAccount.Actor.Avatars.id",
|
||||
"Abuse->FlaggedAccount->Actor->Avatars"."width" AS "Abuse.FlaggedAccount.Actor.Avatars.width",
|
||||
"Abuse->FlaggedAccount->Actor->Avatars"."filename" AS "Abuse.FlaggedAccount.Actor.Avatars.filename",
|
||||
"Abuse->FlaggedAccount->Actor->Server"."id" AS "Abuse.FlaggedAccount.Actor.Server.id",
|
||||
"Abuse->FlaggedAccount->Actor->Server"."host" AS "Abuse.FlaggedAccount.Actor.Server.host",
|
||||
"VideoBlacklist"."id" AS "VideoBlacklist.id",
|
||||
"VideoBlacklist->Video"."id" AS "VideoBlacklist.Video.id",
|
||||
"VideoBlacklist->Video"."uuid" AS "VideoBlacklist.Video.uuid",
|
||||
"VideoBlacklist->Video"."name" AS "VideoBlacklist.Video.name",
|
||||
"VideoImport"."id" AS "VideoImport.id",
|
||||
"VideoImport"."magnetUri" AS "VideoImport.magnetUri",
|
||||
"VideoImport"."targetUrl" AS "VideoImport.targetUrl",
|
||||
"VideoImport"."torrentName" AS "VideoImport.torrentName",
|
||||
"VideoImport->Video"."id" AS "VideoImport.Video.id",
|
||||
"VideoImport->Video"."uuid" AS "VideoImport.Video.uuid",
|
||||
"VideoImport->Video"."name" AS "VideoImport.Video.name",
|
||||
"Plugin"."id" AS "Plugin.id",
|
||||
"Plugin"."name" AS "Plugin.name",
|
||||
"Plugin"."type" AS "Plugin.type",
|
||||
"Plugin"."latestVersion" AS "Plugin.latestVersion",
|
||||
"Application"."id" AS "Application.id",
|
||||
"Application"."latestPeerTubeVersion" AS "Application.latestPeerTubeVersion",
|
||||
"ActorFollow"."id" AS "ActorFollow.id",
|
||||
"ActorFollow"."state" AS "ActorFollow.state",
|
||||
"ActorFollow->ActorFollower"."id" AS "ActorFollow.ActorFollower.id",
|
||||
"ActorFollow->ActorFollower"."preferredUsername" AS "ActorFollow.ActorFollower.preferredUsername",
|
||||
"ActorFollow->ActorFollower->Account"."id" AS "ActorFollow.ActorFollower.Account.id",
|
||||
"ActorFollow->ActorFollower->Account"."name" AS "ActorFollow.ActorFollower.Account.name",
|
||||
"ActorFollow->ActorFollower->Avatars"."id" AS "ActorFollow.ActorFollower.Avatars.id",
|
||||
"ActorFollow->ActorFollower->Avatars"."width" AS "ActorFollow.ActorFollower.Avatars.width",
|
||||
"ActorFollow->ActorFollower->Avatars"."filename" AS "ActorFollow.ActorFollower.Avatars.filename",
|
||||
"ActorFollow->ActorFollower->Server"."id" AS "ActorFollow.ActorFollower.Server.id",
|
||||
"ActorFollow->ActorFollower->Server"."host" AS "ActorFollow.ActorFollower.Server.host",
|
||||
"ActorFollow->ActorFollowing"."id" AS "ActorFollow.ActorFollowing.id",
|
||||
"ActorFollow->ActorFollowing"."preferredUsername" AS "ActorFollow.ActorFollowing.preferredUsername",
|
||||
"ActorFollow->ActorFollowing"."type" AS "ActorFollow.ActorFollowing.type",
|
||||
"ActorFollow->ActorFollowing->VideoChannel"."id" AS "ActorFollow.ActorFollowing.VideoChannel.id",
|
||||
"ActorFollow->ActorFollowing->VideoChannel"."name" AS "ActorFollow.ActorFollowing.VideoChannel.name",
|
||||
"ActorFollow->ActorFollowing->Account"."id" AS "ActorFollow.ActorFollowing.Account.id",
|
||||
"ActorFollow->ActorFollowing->Account"."name" AS "ActorFollow.ActorFollowing.Account.name",
|
||||
"ActorFollow->ActorFollowing->Server"."id" AS "ActorFollow.ActorFollowing.Server.id",
|
||||
"ActorFollow->ActorFollowing->Server"."host" AS "ActorFollow.ActorFollowing.Server.host",
|
||||
"Account"."id" AS "Account.id",
|
||||
"Account"."name" AS "Account.name",
|
||||
"Account->Actor"."id" AS "Account.Actor.id",
|
||||
"Account->Actor"."preferredUsername" AS "Account.Actor.preferredUsername",
|
||||
"Account->Actor->Avatars"."id" AS "Account.Actor.Avatars.id",
|
||||
"Account->Actor->Avatars"."width" AS "Account.Actor.Avatars.width",
|
||||
"Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename",
|
||||
"Account->Actor->Server"."id" AS "Account.Actor.Server.id",
|
||||
"Account->Actor->Server"."host" AS "Account.Actor.Server.host"`
|
||||
}
|
||||
|
||||
private getJoins () {
|
||||
return `
|
||||
LEFT JOIN (
|
||||
"video" AS "Video"
|
||||
INNER JOIN "videoChannel" AS "Video->VideoChannel" ON "Video"."channelId" = "Video->VideoChannel"."id"
|
||||
INNER JOIN "actor" AS "Video->VideoChannel->Actor" ON "Video->VideoChannel"."actorId" = "Video->VideoChannel->Actor"."id"
|
||||
LEFT JOIN "actorImage" AS "Video->VideoChannel->Actor->Avatars"
|
||||
ON "Video->VideoChannel->Actor"."id" = "Video->VideoChannel->Actor->Avatars"."actorId"
|
||||
AND "Video->VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||
LEFT JOIN "server" AS "Video->VideoChannel->Actor->Server"
|
||||
ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id"
|
||||
) ON "UserNotificationModel"."videoId" = "Video"."id"
|
||||
|
||||
LEFT JOIN (
|
||||
"videoComment" AS "VideoComment"
|
||||
INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id"
|
||||
INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id"
|
||||
LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars"
|
||||
ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId"
|
||||
AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||
LEFT JOIN "server" AS "VideoComment->Account->Actor->Server"
|
||||
ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id"
|
||||
INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id"
|
||||
) ON "UserNotificationModel"."commentId" = "VideoComment"."id"
|
||||
|
||||
LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id"
|
||||
LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId"
|
||||
LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id"
|
||||
LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId"
|
||||
LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment"
|
||||
ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id"
|
||||
LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video"
|
||||
ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id"
|
||||
LEFT JOIN (
|
||||
"account" AS "Abuse->FlaggedAccount"
|
||||
INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id"
|
||||
LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars"
|
||||
ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId"
|
||||
AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||
LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server"
|
||||
ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id"
|
||||
) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id"
|
||||
|
||||
LEFT JOIN (
|
||||
"videoBlacklist" AS "VideoBlacklist"
|
||||
INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id"
|
||||
) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id"
|
||||
|
||||
LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id"
|
||||
LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id"
|
||||
|
||||
LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id"
|
||||
|
||||
LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id"
|
||||
|
||||
LEFT JOIN (
|
||||
"actorFollow" AS "ActorFollow"
|
||||
INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id"
|
||||
INNER JOIN "account" AS "ActorFollow->ActorFollower->Account"
|
||||
ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId"
|
||||
LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars"
|
||||
ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId"
|
||||
AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||
LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server"
|
||||
ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id"
|
||||
INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id"
|
||||
LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel"
|
||||
ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId"
|
||||
LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account"
|
||||
ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId"
|
||||
LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server"
|
||||
ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id"
|
||||
) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id"
|
||||
|
||||
LEFT JOIN (
|
||||
"account" AS "Account"
|
||||
INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id"
|
||||
LEFT JOIN "actorImage" AS "Account->Actor->Avatars"
|
||||
ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId"
|
||||
AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||
LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"
|
||||
) ON "UserNotificationModel"."accountId" = "Account"."id"`
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
|
||||
import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
|
||||
import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { getBiggestActorImage } from '@server/lib/actor-image'
|
||||
import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
|
||||
import { uuidToShort } from '@shared/extra-utils'
|
||||
import { UserNotification, UserNotificationType } from '@shared/models'
|
||||
|
@ -7,207 +8,18 @@ import { AttributesOnly } from '@shared/typescript-utils'
|
|||
import { isBooleanValid } from '../../helpers/custom-validators/misc'
|
||||
import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
|
||||
import { AbuseModel } from '../abuse/abuse'
|
||||
import { VideoAbuseModel } from '../abuse/video-abuse'
|
||||
import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
|
||||
import { AccountModel } from '../account/account'
|
||||
import { ActorModel } from '../actor/actor'
|
||||
import { ActorFollowModel } from '../actor/actor-follow'
|
||||
import { ActorImageModel } from '../actor/actor-image'
|
||||
import { ApplicationModel } from '../application/application'
|
||||
import { PluginModel } from '../server/plugin'
|
||||
import { ServerModel } from '../server/server'
|
||||
import { getSort, throwIfNotValid } from '../utils'
|
||||
import { throwIfNotValid } from '../utils'
|
||||
import { VideoModel } from '../video/video'
|
||||
import { VideoBlacklistModel } from '../video/video-blacklist'
|
||||
import { VideoChannelModel } from '../video/video-channel'
|
||||
import { VideoCommentModel } from '../video/video-comment'
|
||||
import { VideoImportModel } from '../video/video-import'
|
||||
import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder'
|
||||
import { UserModel } from './user'
|
||||
|
||||
enum ScopeNames {
|
||||
WITH_ALL = 'WITH_ALL'
|
||||
}
|
||||
|
||||
function buildActorWithAvatarInclude () {
|
||||
return {
|
||||
attributes: [ 'preferredUsername' ],
|
||||
model: ActorModel.unscoped(),
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'filename' ],
|
||||
as: 'Avatar',
|
||||
model: ActorImageModel.unscoped(),
|
||||
required: false
|
||||
},
|
||||
{
|
||||
attributes: [ 'host' ],
|
||||
model: ServerModel.unscoped(),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function buildVideoInclude (required: boolean) {
|
||||
return {
|
||||
attributes: [ 'id', 'uuid', 'name' ],
|
||||
model: VideoModel.unscoped(),
|
||||
required
|
||||
}
|
||||
}
|
||||
|
||||
function buildChannelInclude (required: boolean, withActor = false) {
|
||||
return {
|
||||
required,
|
||||
attributes: [ 'id', 'name' ],
|
||||
model: VideoChannelModel.unscoped(),
|
||||
include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
|
||||
}
|
||||
}
|
||||
|
||||
function buildAccountInclude (required: boolean, withActor = false) {
|
||||
return {
|
||||
required,
|
||||
attributes: [ 'id', 'name' ],
|
||||
model: AccountModel.unscoped(),
|
||||
include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
|
||||
}
|
||||
}
|
||||
|
||||
@Scopes(() => ({
|
||||
[ScopeNames.WITH_ALL]: {
|
||||
include: [
|
||||
Object.assign(buildVideoInclude(false), {
|
||||
include: [ buildChannelInclude(true, true) ]
|
||||
}),
|
||||
|
||||
{
|
||||
attributes: [ 'id', 'originCommentId' ],
|
||||
model: VideoCommentModel.unscoped(),
|
||||
required: false,
|
||||
include: [
|
||||
buildAccountInclude(true, true),
|
||||
buildVideoInclude(true)
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
attributes: [ 'id', 'state' ],
|
||||
model: AbuseModel.unscoped(),
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'id' ],
|
||||
model: VideoAbuseModel.unscoped(),
|
||||
required: false,
|
||||
include: [ buildVideoInclude(false) ]
|
||||
},
|
||||
{
|
||||
attributes: [ 'id' ],
|
||||
model: VideoCommentAbuseModel.unscoped(),
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'id', 'originCommentId' ],
|
||||
model: VideoCommentModel.unscoped(),
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'id', 'name', 'uuid' ],
|
||||
model: VideoModel.unscoped(),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: AccountModel,
|
||||
as: 'FlaggedAccount',
|
||||
required: false,
|
||||
include: [ buildActorWithAvatarInclude() ]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
attributes: [ 'id' ],
|
||||
model: VideoBlacklistModel.unscoped(),
|
||||
required: false,
|
||||
include: [ buildVideoInclude(true) ]
|
||||
},
|
||||
|
||||
{
|
||||
attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
|
||||
model: VideoImportModel.unscoped(),
|
||||
required: false,
|
||||
include: [ buildVideoInclude(false) ]
|
||||
},
|
||||
|
||||
{
|
||||
attributes: [ 'id', 'name', 'type', 'latestVersion' ],
|
||||
model: PluginModel.unscoped(),
|
||||
required: false
|
||||
},
|
||||
|
||||
{
|
||||
attributes: [ 'id', 'latestPeerTubeVersion' ],
|
||||
model: ApplicationModel.unscoped(),
|
||||
required: false
|
||||
},
|
||||
|
||||
{
|
||||
attributes: [ 'id', 'state' ],
|
||||
model: ActorFollowModel.unscoped(),
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'preferredUsername' ],
|
||||
model: ActorModel.unscoped(),
|
||||
required: true,
|
||||
as: 'ActorFollower',
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'id', 'name' ],
|
||||
model: AccountModel.unscoped(),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
attributes: [ 'filename' ],
|
||||
as: 'Avatar',
|
||||
model: ActorImageModel.unscoped(),
|
||||
required: false
|
||||
},
|
||||
{
|
||||
attributes: [ 'host' ],
|
||||
model: ServerModel.unscoped(),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
attributes: [ 'preferredUsername', 'type' ],
|
||||
model: ActorModel.unscoped(),
|
||||
required: true,
|
||||
as: 'ActorFollowing',
|
||||
include: [
|
||||
buildChannelInclude(false),
|
||||
buildAccountInclude(false),
|
||||
{
|
||||
attributes: [ 'host' ],
|
||||
model: ServerModel.unscoped(),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
buildAccountInclude(false, true)
|
||||
]
|
||||
}
|
||||
}))
|
||||
@Table({
|
||||
tableName: 'userNotification',
|
||||
indexes: [
|
||||
|
@ -342,7 +154,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
|||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
Comment: VideoCommentModel
|
||||
VideoComment: VideoCommentModel
|
||||
|
||||
@ForeignKey(() => AbuseModel)
|
||||
@Column
|
||||
|
@ -431,11 +243,14 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
|||
static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
|
||||
const where = { userId }
|
||||
|
||||
const query: FindOptions = {
|
||||
const query = {
|
||||
userId,
|
||||
unread,
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getSort(sort),
|
||||
where
|
||||
sort,
|
||||
where,
|
||||
sequelize: this.sequelize
|
||||
}
|
||||
|
||||
if (unread !== undefined) query.where['read'] = !unread
|
||||
|
@ -445,8 +260,8 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
|||
.then(count => count || 0),
|
||||
|
||||
count === 0
|
||||
? []
|
||||
: UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query)
|
||||
? [] as UserNotificationModelForApi[]
|
||||
: new UserNotificationListQueryBuilder(query).listNotifications()
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
|
@ -524,25 +339,31 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
|||
|
||||
toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
|
||||
const video = this.Video
|
||||
? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) })
|
||||
? {
|
||||
...this.formatVideo(this.Video),
|
||||
|
||||
channel: this.formatActor(this.Video.VideoChannel)
|
||||
}
|
||||
: undefined
|
||||
|
||||
const videoImport = this.VideoImport
|
||||
? {
|
||||
id: this.VideoImport.id,
|
||||
video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
|
||||
video: this.VideoImport.Video
|
||||
? this.formatVideo(this.VideoImport.Video)
|
||||
: undefined,
|
||||
torrentName: this.VideoImport.torrentName,
|
||||
magnetUri: this.VideoImport.magnetUri,
|
||||
targetUrl: this.VideoImport.targetUrl
|
||||
}
|
||||
: undefined
|
||||
|
||||
const comment = this.Comment
|
||||
const comment = this.VideoComment
|
||||
? {
|
||||
id: this.Comment.id,
|
||||
threadId: this.Comment.getThreadId(),
|
||||
account: this.formatActor(this.Comment.Account),
|
||||
video: this.formatVideo(this.Comment.Video)
|
||||
id: this.VideoComment.id,
|
||||
threadId: this.VideoComment.getThreadId(),
|
||||
account: this.formatActor(this.VideoComment.Account),
|
||||
video: this.formatVideo(this.VideoComment.Video)
|
||||
}
|
||||
: undefined
|
||||
|
||||
|
@ -570,8 +391,9 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
|||
id: this.ActorFollow.ActorFollower.Account.id,
|
||||
displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
|
||||
name: this.ActorFollow.ActorFollower.preferredUsername,
|
||||
avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined,
|
||||
host: this.ActorFollow.ActorFollower.getHost()
|
||||
host: this.ActorFollow.ActorFollower.getHost(),
|
||||
|
||||
...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars)
|
||||
},
|
||||
following: {
|
||||
type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
|
||||
|
@ -612,7 +434,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
|||
}
|
||||
}
|
||||
|
||||
formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) {
|
||||
formatVideo (video: UserNotificationIncludes.VideoInclude) {
|
||||
return {
|
||||
id: video.id,
|
||||
uuid: video.uuid,
|
||||
|
@ -621,7 +443,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
|||
}
|
||||
}
|
||||
|
||||
formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) {
|
||||
formatAbuse (abuse: UserNotificationIncludes.AbuseInclude) {
|
||||
const commentAbuse = abuse.VideoCommentAbuse?.VideoComment
|
||||
? {
|
||||
threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
|
||||
|
@ -637,9 +459,13 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
|||
}
|
||||
: undefined
|
||||
|
||||
const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
|
||||
const videoAbuse = abuse.VideoAbuse?.Video
|
||||
? this.formatVideo(abuse.VideoAbuse.Video)
|
||||
: undefined
|
||||
|
||||
const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined
|
||||
const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount)
|
||||
? this.formatActor(abuse.FlaggedAccount)
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id: abuse.id,
|
||||
|
@ -651,19 +477,32 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
|||
}
|
||||
|
||||
formatActor (
|
||||
this: UserNotificationModelForApi,
|
||||
accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
|
||||
) {
|
||||
const avatar = accountOrChannel.Actor.Avatar
|
||||
? { path: accountOrChannel.Actor.Avatar.getStaticPath() }
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id: accountOrChannel.id,
|
||||
displayName: accountOrChannel.getDisplayName(),
|
||||
name: accountOrChannel.Actor.preferredUsername,
|
||||
host: accountOrChannel.Actor.getHost(),
|
||||
avatar
|
||||
|
||||
...this.formatAvatars(accountOrChannel.Actor.Avatars)
|
||||
}
|
||||
}
|
||||
|
||||
formatAvatars (avatars: UserNotificationIncludes.ActorImageInclude[]) {
|
||||
if (!avatars || avatars.length === 0) return { avatar: undefined, avatars: [] }
|
||||
|
||||
return {
|
||||
avatar: this.formatAvatar(getBiggestActorImage(avatars)),
|
||||
|
||||
avatars: avatars.map(a => this.formatAvatar(a))
|
||||
}
|
||||
}
|
||||
|
||||
formatAvatar (a: UserNotificationIncludes.ActorImageInclude) {
|
||||
return {
|
||||
path: a.getStaticPath(),
|
||||
width: a.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ enum ScopeNames {
|
|||
include: [
|
||||
{
|
||||
model: ActorImageModel,
|
||||
as: 'Banner',
|
||||
as: 'Banners',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
|
@ -495,13 +495,10 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
|
|||
where
|
||||
}
|
||||
|
||||
return UserModel.findAndCountAll(query)
|
||||
.then(({ rows, count }) => {
|
||||
return {
|
||||
data: rows,
|
||||
total: count
|
||||
}
|
||||
})
|
||||
return Promise.all([
|
||||
UserModel.unscoped().count(query),
|
||||
UserModel.findAll(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static listWithRight (right: UserRight): Promise<MUserDefault[]> {
|
||||
|
|
|
@ -181,7 +181,7 @@ function buildServerIdsFollowedBy (actorId: any) {
|
|||
'SELECT "actor"."serverId" FROM "actorFollow" ' +
|
||||
'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' +
|
||||
'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
|
||||
')'
|
||||
')'
|
||||
}
|
||||
|
||||
function buildWhereIdOrUUID (id: number | string) {
|
||||
|
|
3
server/models/video/sql/video/index.ts
Normal file
3
server/models/video/sql/video/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './video-model-get-query-builder'
|
||||
export * from './videos-id-list-query-builder'
|
||||
export * from './videos-model-list-query-builder'
|
|
@ -1,5 +1,6 @@
|
|||
import { createSafeIn } from '@server/models/utils'
|
||||
import { MUserAccountId } from '@server/types/models'
|
||||
import { ActorImageType } from '@shared/models'
|
||||
import validator from 'validator'
|
||||
import { AbstractRunQuery } from './abstract-run-query'
|
||||
import { VideoTableAttributes } from './video-table-attributes'
|
||||
|
@ -42,8 +43,9 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
|
|||
)
|
||||
|
||||
this.addJoin(
|
||||
'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' +
|
||||
'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"'
|
||||
'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatars" ' +
|
||||
'ON "VideoChannel->Actor"."id" = "VideoChannel->Actor->Avatars"."actorId" ' +
|
||||
`AND "VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR}`
|
||||
)
|
||||
|
||||
this.attributes = {
|
||||
|
@ -51,7 +53,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
|
|||
|
||||
...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()),
|
||||
...this.buildActorInclude('VideoChannel->Actor'),
|
||||
...this.buildAvatarInclude('VideoChannel->Actor->Avatar'),
|
||||
...this.buildAvatarInclude('VideoChannel->Actor->Avatars'),
|
||||
...this.buildServerInclude('VideoChannel->Actor->Server')
|
||||
}
|
||||
}
|
||||
|
@ -68,8 +70,9 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
|
|||
)
|
||||
|
||||
this.addJoin(
|
||||
'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' +
|
||||
'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"'
|
||||
'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatars" ' +
|
||||
'ON "VideoChannel->Account"."actorId"= "VideoChannel->Account->Actor->Avatars"."actorId" ' +
|
||||
`AND "VideoChannel->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}`
|
||||
)
|
||||
|
||||
this.attributes = {
|
||||
|
@ -77,7 +80,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
|
|||
|
||||
...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()),
|
||||
...this.buildActorInclude('VideoChannel->Account->Actor'),
|
||||
...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatar'),
|
||||
...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatars'),
|
||||
...this.buildServerInclude('VideoChannel->Account->Actor->Server')
|
||||
}
|
||||
}
|
|
@ -9,15 +9,15 @@ import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
|
|||
import { TrackerModel } from '@server/models/server/tracker'
|
||||
import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
|
||||
import { VideoInclude } from '@shared/models'
|
||||
import { ScheduleVideoUpdateModel } from '../../schedule-video-update'
|
||||
import { TagModel } from '../../tag'
|
||||
import { ThumbnailModel } from '../../thumbnail'
|
||||
import { VideoModel } from '../../video'
|
||||
import { VideoBlacklistModel } from '../../video-blacklist'
|
||||
import { VideoChannelModel } from '../../video-channel'
|
||||
import { VideoFileModel } from '../../video-file'
|
||||
import { VideoLiveModel } from '../../video-live'
|
||||
import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist'
|
||||
import { ScheduleVideoUpdateModel } from '../../../schedule-video-update'
|
||||
import { TagModel } from '../../../tag'
|
||||
import { ThumbnailModel } from '../../../thumbnail'
|
||||
import { VideoModel } from '../../../video'
|
||||
import { VideoBlacklistModel } from '../../../video-blacklist'
|
||||
import { VideoChannelModel } from '../../../video-channel'
|
||||
import { VideoFileModel } from '../../../video-file'
|
||||
import { VideoLiveModel } from '../../../video-live'
|
||||
import { VideoStreamingPlaylistModel } from '../../../video-streaming-playlist'
|
||||
import { VideoTableAttributes } from './video-table-attributes'
|
||||
|
||||
type SQLRow = { [id: string]: string | number }
|
||||
|
@ -34,6 +34,7 @@ export class VideoModelBuilder {
|
|||
private videoFileMemo: { [ id: number ]: VideoFileModel }
|
||||
|
||||
private thumbnailsDone: Set<any>
|
||||
private actorImagesDone: Set<any>
|
||||
private historyDone: Set<any>
|
||||
private blacklistDone: Set<any>
|
||||
private accountBlocklistDone: Set<any>
|
||||
|
@ -69,11 +70,21 @@ export class VideoModelBuilder {
|
|||
for (const row of rows) {
|
||||
this.buildVideoAndAccount(row)
|
||||
|
||||
const videoModel = this.videosMemo[row.id]
|
||||
const videoModel = this.videosMemo[row.id as number]
|
||||
|
||||
this.setUserHistory(row, videoModel)
|
||||
this.addThumbnail(row, videoModel)
|
||||
|
||||
const channelActor = videoModel.VideoChannel?.Actor
|
||||
if (channelActor) {
|
||||
this.addActorAvatar(row, 'VideoChannel.Actor', channelActor)
|
||||
}
|
||||
|
||||
const accountActor = videoModel.VideoChannel?.Account?.Actor
|
||||
if (accountActor) {
|
||||
this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor)
|
||||
}
|
||||
|
||||
if (!rowsWebTorrentFiles) {
|
||||
this.addWebTorrentFile(row, videoModel)
|
||||
}
|
||||
|
@ -113,6 +124,7 @@ export class VideoModelBuilder {
|
|||
this.videoFileMemo = {}
|
||||
|
||||
this.thumbnailsDone = new Set()
|
||||
this.actorImagesDone = new Set()
|
||||
this.historyDone = new Set()
|
||||
this.blacklistDone = new Set()
|
||||
this.liveDone = new Set()
|
||||
|
@ -195,13 +207,8 @@ export class VideoModelBuilder {
|
|||
|
||||
private buildActor (row: SQLRow, prefix: string) {
|
||||
const actorPrefix = `${prefix}.Actor`
|
||||
const avatarPrefix = `${actorPrefix}.Avatar`
|
||||
const serverPrefix = `${actorPrefix}.Server`
|
||||
|
||||
const avatarModel = row[`${avatarPrefix}.id`] !== null
|
||||
? new ActorImageModel(this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix), this.buildOpts)
|
||||
: null
|
||||
|
||||
const serverModel = row[`${serverPrefix}.id`] !== null
|
||||
? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts)
|
||||
: null
|
||||
|
@ -209,8 +216,8 @@ export class VideoModelBuilder {
|
|||
if (serverModel) serverModel.BlockedBy = []
|
||||
|
||||
const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts)
|
||||
actorModel.Avatar = avatarModel
|
||||
actorModel.Server = serverModel
|
||||
actorModel.Avatars = []
|
||||
|
||||
return actorModel
|
||||
}
|
||||
|
@ -226,6 +233,18 @@ export class VideoModelBuilder {
|
|||
this.historyDone.add(id)
|
||||
}
|
||||
|
||||
private addActorAvatar (row: SQLRow, actorPrefix: string, actor: ActorModel) {
|
||||
const avatarPrefix = `${actorPrefix}.Avatar`
|
||||
const id = row[`${avatarPrefix}.id`]
|
||||
if (!id || this.actorImagesDone.has(id)) return
|
||||
|
||||
const attributes = this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix)
|
||||
const avatarModel = new ActorImageModel(attributes, this.buildOpts)
|
||||
actor.Avatars.push(avatarModel)
|
||||
|
||||
this.actorImagesDone.add(id)
|
||||
}
|
||||
|
||||
private addThumbnail (row: SQLRow, videoModel: VideoModel) {
|
||||
const id = row['Thumbnails.id']
|
||||
if (!id || this.thumbnailsDone.has(id)) return
|
|
@ -186,8 +186,7 @@ export class VideoTableAttributes {
|
|||
'id',
|
||||
'preferredUsername',
|
||||
'url',
|
||||
'serverId',
|
||||
'avatarId'
|
||||
'serverId'
|
||||
]
|
||||
|
||||
if (this.mode === 'get') {
|
||||
|
@ -212,6 +211,7 @@ export class VideoTableAttributes {
|
|||
getAvatarAttributes () {
|
||||
let attributeKeys = [
|
||||
'id',
|
||||
'width',
|
||||
'filename',
|
||||
'type',
|
||||
'fileUrl',
|
|
@ -31,6 +31,7 @@ import {
|
|||
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
||||
import { sendDeleteActor } from '../../lib/activitypub/send'
|
||||
import {
|
||||
MChannel,
|
||||
MChannelActor,
|
||||
MChannelAP,
|
||||
MChannelBannerAccountDefault,
|
||||
|
@ -62,6 +63,7 @@ type AvailableForListOptions = {
|
|||
search?: string
|
||||
host?: string
|
||||
handles?: string[]
|
||||
forCount?: boolean
|
||||
}
|
||||
|
||||
type AvailableWithStatsOptions = {
|
||||
|
@ -116,70 +118,91 @@ export type SummaryOptions = {
|
|||
})
|
||||
}
|
||||
|
||||
let rootWhere: WhereOptions
|
||||
if (options.handles) {
|
||||
const or: WhereOptions[] = []
|
||||
if (Array.isArray(options.handles) && options.handles.length !== 0) {
|
||||
const or: string[] = []
|
||||
|
||||
for (const handle of options.handles || []) {
|
||||
const [ preferredUsername, host ] = handle.split('@')
|
||||
|
||||
if (!host || host === WEBSERVER.HOST) {
|
||||
or.push({
|
||||
'$Actor.preferredUsername$': preferredUsername,
|
||||
'$Actor.serverId$': null
|
||||
})
|
||||
or.push(`("preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} AND "serverId" IS NULL)`)
|
||||
} else {
|
||||
or.push({
|
||||
'$Actor.preferredUsername$': preferredUsername,
|
||||
'$Actor.Server.host$': host
|
||||
})
|
||||
or.push(
|
||||
`(` +
|
||||
`"preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} ` +
|
||||
`AND "host" = ${VideoChannelModel.sequelize.escape(host)}` +
|
||||
`)`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
rootWhere = {
|
||||
[Op.or]: or
|
||||
}
|
||||
whereActorAnd.push({
|
||||
id: {
|
||||
[Op.in]: literal(`(SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" WHERE ${or.join(' OR ')})`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const channelInclude: Includeable[] = []
|
||||
const accountInclude: Includeable[] = []
|
||||
|
||||
if (options.forCount !== true) {
|
||||
accountInclude.push({
|
||||
model: ServerModel,
|
||||
required: false
|
||||
})
|
||||
|
||||
accountInclude.push({
|
||||
model: ActorImageModel,
|
||||
as: 'Avatars',
|
||||
required: false
|
||||
})
|
||||
|
||||
channelInclude.push({
|
||||
model: ActorImageModel,
|
||||
as: 'Avatars',
|
||||
required: false
|
||||
})
|
||||
|
||||
channelInclude.push({
|
||||
model: ActorImageModel,
|
||||
as: 'Banners',
|
||||
required: false
|
||||
})
|
||||
}
|
||||
|
||||
if (options.forCount !== true || serverRequired) {
|
||||
channelInclude.push({
|
||||
model: ServerModel,
|
||||
duplicating: false,
|
||||
required: serverRequired,
|
||||
where: whereServer
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
where: rootWhere,
|
||||
include: [
|
||||
{
|
||||
attributes: {
|
||||
exclude: unusedActorAttributesForAPI
|
||||
},
|
||||
model: ActorModel,
|
||||
model: ActorModel.unscoped(),
|
||||
where: {
|
||||
[Op.and]: whereActorAnd
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: ServerModel,
|
||||
required: serverRequired,
|
||||
where: whereServer
|
||||
},
|
||||
{
|
||||
model: ActorImageModel,
|
||||
as: 'Avatar',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: ActorImageModel,
|
||||
as: 'Banner',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
include: channelInclude
|
||||
},
|
||||
{
|
||||
model: AccountModel,
|
||||
model: AccountModel.unscoped(),
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
attributes: {
|
||||
exclude: unusedActorAttributesForAPI
|
||||
},
|
||||
model: ActorModel, // Default scope includes avatar and server
|
||||
required: true
|
||||
model: ActorModel.unscoped(),
|
||||
required: true,
|
||||
include: accountInclude
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -189,7 +212,7 @@ export type SummaryOptions = {
|
|||
[ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
|
||||
const include: Includeable[] = [
|
||||
{
|
||||
attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
|
||||
attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ],
|
||||
model: ActorModel.unscoped(),
|
||||
required: options.actorRequired ?? true,
|
||||
include: [
|
||||
|
@ -199,8 +222,8 @@ export type SummaryOptions = {
|
|||
required: false
|
||||
},
|
||||
{
|
||||
model: ActorImageModel.unscoped(),
|
||||
as: 'Avatar',
|
||||
model: ActorImageModel,
|
||||
as: 'Avatars',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
|
@ -245,7 +268,7 @@ export type SummaryOptions = {
|
|||
{
|
||||
model: ActorImageModel,
|
||||
required: false,
|
||||
as: 'Banner'
|
||||
as: 'Banners'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -474,14 +497,14 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
order: getSort(parameters.sort)
|
||||
}
|
||||
|
||||
return VideoChannelModel
|
||||
.scope({
|
||||
method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
|
||||
})
|
||||
.findAndCountAll(query)
|
||||
.then(({ rows, count }) => {
|
||||
return { total: count, data: rows }
|
||||
})
|
||||
const getScope = (forCount: boolean) => {
|
||||
return { method: [ ScopeNames.FOR_API, { actorId, forCount } as AvailableForListOptions ] }
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
VideoChannelModel.scope(getScope(true)).count(),
|
||||
VideoChannelModel.scope(getScope(false)).findAll(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & {
|
||||
|
@ -519,14 +542,22 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
where
|
||||
}
|
||||
|
||||
return VideoChannelModel
|
||||
.scope({
|
||||
method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ]
|
||||
})
|
||||
.findAndCountAll(query)
|
||||
.then(({ rows, count }) => {
|
||||
return { total: count, data: rows }
|
||||
})
|
||||
const getScope = (forCount: boolean) => {
|
||||
return {
|
||||
method: [
|
||||
ScopeNames.FOR_API, {
|
||||
...pick(options, [ 'actorId', 'host', 'handles' ]),
|
||||
|
||||
forCount
|
||||
} as AvailableForListOptions
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
VideoChannelModel.scope(getScope(true)).count(query),
|
||||
VideoChannelModel.scope(getScope(false)).findAll(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static listByAccountForAPI (options: {
|
||||
|
@ -552,20 +583,26 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
}
|
||||
: null
|
||||
|
||||
const query = {
|
||||
offset: options.start,
|
||||
limit: options.count,
|
||||
order: getSort(options.sort),
|
||||
include: [
|
||||
{
|
||||
model: AccountModel,
|
||||
where: {
|
||||
id: options.accountId
|
||||
},
|
||||
required: true
|
||||
}
|
||||
],
|
||||
where
|
||||
const getQuery = (forCount: boolean) => {
|
||||
const accountModel = forCount
|
||||
? AccountModel.unscoped()
|
||||
: AccountModel
|
||||
|
||||
return {
|
||||
offset: options.start,
|
||||
limit: options.count,
|
||||
order: getSort(options.sort),
|
||||
include: [
|
||||
{
|
||||
model: accountModel,
|
||||
where: {
|
||||
id: options.accountId
|
||||
},
|
||||
required: true
|
||||
}
|
||||
],
|
||||
where
|
||||
}
|
||||
}
|
||||
|
||||
const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ]
|
||||
|
@ -576,21 +613,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
})
|
||||
}
|
||||
|
||||
return VideoChannelModel
|
||||
.scope(scopes)
|
||||
.findAndCountAll(query)
|
||||
.then(({ rows, count }) => {
|
||||
return { total: count, data: rows }
|
||||
})
|
||||
return Promise.all([
|
||||
VideoChannelModel.scope(scopes).count(getQuery(true)),
|
||||
VideoChannelModel.scope(scopes).findAll(getQuery(false))
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static listAllByAccount (accountId: number) {
|
||||
static listAllByAccount (accountId: number): Promise<MChannel[]> {
|
||||
const query = {
|
||||
limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER,
|
||||
include: [
|
||||
{
|
||||
attributes: [],
|
||||
model: AccountModel,
|
||||
model: AccountModel.unscoped(),
|
||||
where: {
|
||||
id: accountId
|
||||
},
|
||||
|
@ -621,7 +656,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
{
|
||||
model: ActorImageModel,
|
||||
required: false,
|
||||
as: 'Banner'
|
||||
as: 'Banners'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -655,7 +690,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
{
|
||||
model: ActorImageModel,
|
||||
required: false,
|
||||
as: 'Banner'
|
||||
as: 'Banners'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -685,7 +720,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
{
|
||||
model: ActorImageModel,
|
||||
required: false,
|
||||
as: 'Banner'
|
||||
as: 'Banners'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -706,6 +741,9 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
displayName: this.getDisplayName(),
|
||||
url: actor.url,
|
||||
host: actor.host,
|
||||
avatars: actor.avatars,
|
||||
|
||||
// TODO: remove, deprecated in 4.2
|
||||
avatar: actor.avatar
|
||||
}
|
||||
}
|
||||
|
@ -736,9 +774,16 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
|
|||
support: this.support,
|
||||
isLocal: this.Actor.isOwned(),
|
||||
updatedAt: this.updatedAt,
|
||||
|
||||
ownerAccount: undefined,
|
||||
|
||||
videosCount,
|
||||
viewsPerDay
|
||||
viewsPerDay,
|
||||
|
||||
avatars: actor.avatars,
|
||||
|
||||
// TODO: remove, deprecated in 4.2
|
||||
avatar: actor.avatar
|
||||
}
|
||||
|
||||
if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { uniq } from 'lodash'
|
||||
import { FindAndCountOptions, FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
|
||||
import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
|
||||
import {
|
||||
AllowNull,
|
||||
BelongsTo,
|
||||
|
@ -16,8 +16,8 @@ import {
|
|||
} from 'sequelize-typescript'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { VideoPrivacy } from '@shared/models'
|
||||
import { AttributesOnly } from '@shared/typescript-utils'
|
||||
import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
|
||||
import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
|
||||
import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model'
|
||||
|
@ -363,40 +363,43 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
|
|||
Object.assign(whereVideo, searchAttribute(searchVideo, 'name'))
|
||||
}
|
||||
|
||||
const query: FindAndCountOptions = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getCommentSort(sort),
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: AccountModel.unscoped(),
|
||||
required: true,
|
||||
where: whereAccount,
|
||||
include: [
|
||||
{
|
||||
attributes: {
|
||||
exclude: unusedActorAttributesForAPI
|
||||
},
|
||||
model: ActorModel, // Default scope includes avatar and server
|
||||
required: true,
|
||||
where: whereActor
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: VideoModel.unscoped(),
|
||||
required: true,
|
||||
where: whereVideo
|
||||
}
|
||||
]
|
||||
const getQuery = (forCount: boolean) => {
|
||||
return {
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getCommentSort(sort),
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: AccountModel.unscoped(),
|
||||
required: true,
|
||||
where: whereAccount,
|
||||
include: [
|
||||
{
|
||||
attributes: {
|
||||
exclude: unusedActorAttributesForAPI
|
||||
},
|
||||
model: forCount === true
|
||||
? ActorModel.unscoped() // Default scope includes avatar and server
|
||||
: ActorModel,
|
||||
required: true,
|
||||
where: whereActor
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: VideoModel.unscoped(),
|
||||
required: true,
|
||||
where: whereVideo
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return VideoCommentModel
|
||||
.findAndCountAll(query)
|
||||
.then(({ rows, count }) => {
|
||||
return { total: count, data: rows }
|
||||
})
|
||||
return Promise.all([
|
||||
VideoCommentModel.count(getQuery(true)),
|
||||
VideoCommentModel.findAll(getQuery(false))
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static async listThreadsForApi (parameters: {
|
||||
|
@ -443,14 +446,20 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
|
|||
}
|
||||
}
|
||||
|
||||
const scopesList: (string | ScopeOptions)[] = [
|
||||
const findScopesList: (string | ScopeOptions)[] = [
|
||||
ScopeNames.WITH_ACCOUNT_FOR_API,
|
||||
{
|
||||
method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
|
||||
}
|
||||
]
|
||||
|
||||
const queryCount = {
|
||||
const countScopesList: ScopeOptions[] = [
|
||||
{
|
||||
method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
|
||||
}
|
||||
]
|
||||
|
||||
const notDeletedQueryCount = {
|
||||
where: {
|
||||
videoId,
|
||||
deletedAt: null,
|
||||
|
@ -459,9 +468,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
|
|||
}
|
||||
|
||||
return Promise.all([
|
||||
VideoCommentModel.scope(scopesList).findAndCountAll(queryList),
|
||||
VideoCommentModel.count(queryCount)
|
||||
]).then(([ { rows, count }, totalNotDeletedComments ]) => {
|
||||
VideoCommentModel.scope(findScopesList).findAll(queryList),
|
||||
VideoCommentModel.scope(countScopesList).count(queryList),
|
||||
VideoCommentModel.count(notDeletedQueryCount)
|
||||
]).then(([ rows, count, totalNotDeletedComments ]) => {
|
||||
return { total: count, data: rows, totalNotDeletedComments }
|
||||
})
|
||||
}
|
||||
|
@ -512,11 +522,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
|
|||
}
|
||||
]
|
||||
|
||||
return VideoCommentModel.scope(scopes)
|
||||
.findAndCountAll(query)
|
||||
.then(({ rows, count }) => {
|
||||
return { total: count, data: rows }
|
||||
})
|
||||
return Promise.all([
|
||||
VideoCommentModel.count(query),
|
||||
VideoCommentModel.scope(scopes).findAll(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> {
|
||||
|
@ -565,7 +574,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
|
|||
transaction: t
|
||||
}
|
||||
|
||||
return VideoCommentModel.findAndCountAll<MComment>(query)
|
||||
return Promise.all([
|
||||
VideoCommentModel.count(query),
|
||||
VideoCommentModel.findAll<MComment>(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static async listForFeed (parameters: {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue