From: Chocobozzz Date: Thu, 11 Jan 2018 08:35:50 +0000 (+0100) Subject: Add scores to follows and remove bad ones X-Git-Tag: v0.0.1-alpha~26 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=60650c77c8a2a98e92d869b237ae4900f369a8fc;p=oweals%2Fpeertube.git Add scores to follows and remove bad ones --- diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html index 9499a0433..d5b1b789d 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.html +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html @@ -3,8 +3,8 @@ sortField="createdAt" (onLazyLoad)="loadLazy($event)" > + - diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts index 2739ff81a..23b46812b 100644 --- a/client/src/app/shared/misc/utils.ts +++ b/client/src/app/shared/misc/utils.ts @@ -31,7 +31,7 @@ function populateAsyncUserVideoChannels (authService: AuthService, channel: any[ const videoChannels = user.videoChannels if (Array.isArray(videoChannels) === false) return - videoChannels.forEach(c => channel.push({ id: c.id, label: c.name })) + videoChannels.forEach(c => channel.push({ id: c.id, label: c.displayName })) return res() } diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html index 2040ff9d4..34291c6c6 100644 --- a/client/src/app/videos/+video-edit/video-add.component.html +++ b/client/src/app/videos/+video-edit/video-add.component.html @@ -44,7 +44,6 @@ [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" > -
Publish will be available when upload is finished
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index a5c387638..5921b4b72 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html @@ -85,7 +85,7 @@
- {{ video.channel.name }} + {{ video.channel.displayName }}
diff --git a/server.ts b/server.ts index a52c47083..99077a173 100644 --- a/server.ts +++ b/server.ts @@ -56,6 +56,7 @@ import { installApplication } from './server/initializers' import { activitypubHttpJobScheduler, transcodingJobScheduler } from './server/lib/jobs' import { VideosPreviewCache } from './server/lib/cache' import { apiRouter, clientsRouter, staticRouter, servicesRouter, webfingerRouter, activityPubRouter } from './server/controllers' +import { BadActorFollowScheduler } from './server/lib/schedulers/bad-actor-follow-scheduler' // ----------- Command line ----------- @@ -168,6 +169,8 @@ function onDatabaseInitDone () { // ----------- Make the server listening ----------- server.listen(port, () => { VideosPreviewCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE) + BadActorFollowScheduler.Instance.enable() + activitypubHttpJobScheduler.activate() transcodingJobScheduler.activate() diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index c735e6daf..0c139912c 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -9,7 +9,7 @@ import { isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 165 +const LAST_MIGRATION_VERSION = 170 // --------------------------------------------------------------------------- @@ -40,12 +40,12 @@ const OAUTH_LIFETIME = { // --------------------------------------------------------------------------- -// Number of points we add/remove from a friend after a successful/bad request -const SERVERS_SCORE = { +// Number of points we add/remove after a successful/bad request +const ACTOR_FOLLOW_SCORE = { PENALTY: -10, BONUS: 10, - BASE: 100, - MAX: 1000 + BASE: 1000, + MAX: 10000 } const FOLLOW_STATES: { [ id: string ]: FollowState } = { @@ -76,6 +76,9 @@ const JOBS_FETCH_LIMIT_PER_CYCLE = { // 1 minutes let JOBS_FETCHING_INTERVAL = 60000 +// 1 hour +let SCHEDULER_INTERVAL = 60000 * 60 + // --------------------------------------------------------------------------- const CONFIG = { @@ -346,7 +349,7 @@ const OPENGRAPH_AND_OEMBED_COMMENT = '' // Special constants for a test instance if (isTestInstance() === true) { - SERVERS_SCORE.BASE = 20 + ACTOR_FOLLOW_SCORE.BASE = 20 JOBS_FETCHING_INTERVAL = 1000 REMOTE_SCHEME.HTTP = 'http' REMOTE_SCHEME.WS = 'ws' @@ -354,6 +357,7 @@ if (isTestInstance() === true) { ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2 ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 60 // 1 minute CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB + SCHEDULER_INTERVAL = 10000 } CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT) @@ -378,7 +382,7 @@ export { OAUTH_LIFETIME, OPENGRAPH_AND_OEMBED_COMMENT, PAGINATION_COUNT_DEFAULT, - SERVERS_SCORE, + ACTOR_FOLLOW_SCORE, PREVIEWS_SIZE, REMOTE_SCHEME, FOLLOW_STATES, @@ -396,5 +400,6 @@ export { VIDEO_LICENCES, VIDEO_RATE_TYPES, VIDEO_MIMETYPE_EXT, - AVATAR_MIMETYPE_EXT + AVATAR_MIMETYPE_EXT, + SCHEDULER_INTERVAL } diff --git a/server/initializers/migrations/0170-actor-follow-score.ts b/server/initializers/migrations/0170-actor-follow-score.ts new file mode 100644 index 000000000..2deabaf98 --- /dev/null +++ b/server/initializers/migrations/0170-actor-follow-score.ts @@ -0,0 +1,28 @@ +import * as Sequelize from 'sequelize' +import { ACTOR_FOLLOW_SCORE } from '../index' + +async function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { + await utils.queryInterface.removeColumn('server', 'score') + + const data = { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: ACTOR_FOLLOW_SCORE.BASE + } + + await utils.queryInterface.addColumn('actorFollow', 'score', data) + +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts index c20a48a4e..3f780e319 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts @@ -1,5 +1,6 @@ import { logger } from '../../../helpers/logger' import { doRequest } from '../../../helpers/requests' +import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { ActivityPubHttpPayload, buildSignedRequestOptions, computeBody, maybeRetryRequestLater } from './activitypub-http-job-scheduler' async function process (payload: ActivityPubHttpPayload, jobId: number) { @@ -15,15 +16,22 @@ async function process (payload: ActivityPubHttpPayload, jobId: number) { httpSignature: httpSignatureOptions } + const badUrls: string[] = [] + const goodUrls: string[] = [] + for (const uri of payload.uris) { options.uri = uri try { await doRequest(options) + goodUrls.push(uri) } catch (err) { - await maybeRetryRequestLater(err, payload, uri) + const isRetryingLater = await maybeRetryRequestLater(err, payload, uri) + if (isRetryingLater === false) badUrls.push(uri) } } + + return ActorFollowModel.updateActorFollowsScoreAndRemoveBadOnes(goodUrls, badUrls, undefined) } function onError (err: Error, jobId: number) { diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts index d576cd42e..884ede5a3 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts @@ -4,6 +4,7 @@ import { logger } from '../../../helpers/logger' import { getServerActor } from '../../../helpers/utils' import { ACTIVITY_PUB } from '../../../initializers' import { ActorModel } from '../../../models/activitypub/actor' +import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { JobHandler, JobScheduler } from '../job-scheduler' import * as activitypubHttpBroadcastHandler from './activitypub-http-broadcast-handler' @@ -26,7 +27,7 @@ const jobCategory: JobCategory = 'activitypub-http' const activitypubHttpJobScheduler = new JobScheduler(jobCategory, jobHandlers) -function maybeRetryRequestLater (err: Error, payload: ActivityPubHttpPayload, uri: string) { +async function maybeRetryRequestLater (err: Error, payload: ActivityPubHttpPayload, uri: string) { logger.warn('Cannot make request to %s.', uri, err) let attemptNumber = payload.attemptNumber || 1 @@ -39,8 +40,12 @@ function maybeRetryRequestLater (err: Error, payload: ActivityPubHttpPayload, ur uris: [ uri ], attemptNumber }) - return activitypubHttpJobScheduler.createJob(undefined, 'activitypubHttpUnicastHandler', newPayload) + await activitypubHttpJobScheduler.createJob(undefined, 'activitypubHttpUnicastHandler', newPayload) + + return true } + + return false } async function computeBody (payload: ActivityPubHttpPayload) { diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts index 175ec6642..e02bd698e 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts @@ -1,5 +1,6 @@ import { logger } from '../../../helpers/logger' import { doRequest } from '../../../helpers/requests' +import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { ActivityPubHttpPayload, buildSignedRequestOptions, computeBody, maybeRetryRequestLater } from './activitypub-http-job-scheduler' async function process (payload: ActivityPubHttpPayload, jobId: number) { @@ -18,8 +19,13 @@ async function process (payload: ActivityPubHttpPayload, jobId: number) { try { await doRequest(options) + await ActorFollowModel.updateActorFollowsScoreAndRemoveBadOnes([ uri ], [], undefined) } catch (err) { - await maybeRetryRequestLater(err, payload, uri) + const isRetryingLater = await maybeRetryRequestLater(err, payload, uri) + if (isRetryingLater === false) { + await ActorFollowModel.updateActorFollowsScoreAndRemoveBadOnes([], [ uri ], undefined) + } + throw err } } diff --git a/server/lib/schedulers/abstract-scheduler.ts b/server/lib/schedulers/abstract-scheduler.ts new file mode 100644 index 000000000..473544ddf --- /dev/null +++ b/server/lib/schedulers/abstract-scheduler.ts @@ -0,0 +1,16 @@ +import { SCHEDULER_INTERVAL } from '../../initializers' + +export abstract class AbstractScheduler { + + private interval: NodeJS.Timer + + enable () { + this.interval = setInterval(() => this.execute(), SCHEDULER_INTERVAL) + } + + disable () { + clearInterval(this.interval) + } + + protected abstract execute () +} diff --git a/server/lib/schedulers/bad-actor-follow-scheduler.ts b/server/lib/schedulers/bad-actor-follow-scheduler.ts new file mode 100644 index 000000000..c6c285ece --- /dev/null +++ b/server/lib/schedulers/bad-actor-follow-scheduler.ts @@ -0,0 +1,24 @@ +import { logger } from '../../helpers/logger' +import { ActorFollowModel } from '../../models/activitypub/actor-follow' +import { AbstractScheduler } from './abstract-scheduler' + +export class BadActorFollowScheduler extends AbstractScheduler { + + private static instance: AbstractScheduler + + private constructor () { + super() + } + + async execute () { + try { + await ActorFollowModel.removeBadActorFollows() + } catch (err) { + logger.error('Error in bad actor follows scheduler.', err) + } + } + + static get Instance () { + return this.instance || (this.instance = new this()) + } +} diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 47336d1e0..f81c50180 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -179,7 +179,6 @@ export class AccountModel extends Model { const actor = this.Actor.toFormattedJSON() const account = { id: this.id, - name: this.Actor.preferredUsername, displayName: this.name, createdAt: this.createdAt, updatedAt: this.updatedAt diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 5fcc3449d..78a65a0ff 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts @@ -1,8 +1,14 @@ import * as Bluebird from 'bluebird' import { values } from 'lodash' import * as Sequelize from 'sequelize' -import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { + AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model, Table, + UpdatedAt +} from 'sequelize-typescript' import { FollowState } from '../../../shared/models/actors' +import { AccountFollow } from '../../../shared/models/actors/follow.model' +import { logger } from '../../helpers/logger' +import { ACTOR_FOLLOW_SCORE } from '../../initializers' import { FOLLOW_STATES } from '../../initializers/constants' import { ServerModel } from '../server/server' import { getSort } from '../utils' @@ -20,6 +26,9 @@ import { ActorModel } from './actor' { fields: [ 'actorId', 'targetActorId' ], unique: true + }, + { + fields: [ 'score' ] } ] }) @@ -29,6 +38,13 @@ export class ActorFollowModel extends Model { @Column(DataType.ENUM(values(FOLLOW_STATES))) state: FollowState + @AllowNull(false) + @Default(ACTOR_FOLLOW_SCORE.BASE) + @IsInt + @Max(ACTOR_FOLLOW_SCORE.MAX) + @Column + score: number + @CreatedAt createdAt: Date @@ -63,6 +79,34 @@ export class ActorFollowModel extends Model { }) ActorFollowing: ActorModel + // Remove actor follows with a score of 0 (too many requests where they were unreachable) + static async removeBadActorFollows () { + const actorFollows = await ActorFollowModel.listBadActorFollows() + + const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy()) + await Promise.all(actorFollowsRemovePromises) + + const numberOfActorFollowsRemoved = actorFollows.length + + if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) + } + + static updateActorFollowsScoreAndRemoveBadOnes (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction) { + if (goodInboxes.length === 0 && badInboxes.length === 0) return + + logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length) + + if (goodInboxes.length !== 0) { + ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t) + .catch(err => logger.error('Cannot increment scores of good actor follows.', err)) + } + + if (badInboxes.length !== 0) { + ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t) + .catch(err => logger.error('Cannot decrement scores of bad actor follows.', err)) + } + } + static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { const query = { where: { @@ -260,7 +304,37 @@ export class ActorFollowModel extends Model { } } - toFormattedJSON () { + private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction) { + const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',') + + const query = 'UPDATE "actorFollow" SET "score" = "score" +' + value + ' ' + + 'WHERE id IN (' + + 'SELECT "actorFollow"."id" FROM "actorFollow" ' + + 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + + 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' + + ')' + + const options = { + type: Sequelize.QueryTypes.BULKUPDATE, + transaction: t + } + + return ActorFollowModel.sequelize.query(query, options) + } + + private static listBadActorFollows () { + const query = { + where: { + score: { + [Sequelize.Op.lte]: 0 + } + } + } + + return ActorFollowModel.findAll(query) + } + + toFormattedJSON (): AccountFollow { const follower = this.ActorFollower.toFormattedJSON() const following = this.ActorFollowing.toFormattedJSON() @@ -268,6 +342,7 @@ export class ActorFollowModel extends Model { id: this.id, follower, following, + score: this.score, state: this.state, createdAt: this.createdAt, updatedAt: this.updatedAt diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index b88e06b41..912d8d748 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -204,7 +204,7 @@ export class ActorModel extends Model { VideoChannel: VideoChannelModel static load (id: number) { - return ActorModel.scope(ScopeNames.FULL).findById(id) + return ActorModel.unscoped().findById(id) } static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { @@ -267,20 +267,17 @@ export class ActorModel extends Model { avatar = this.Avatar.toFormattedJSON() } - let score: number - if (this.Server) { - score = this.Server.score - } - return { id: this.id, url: this.url, uuid: this.uuid, + name: this.preferredUsername, host: this.getHost(), - score, followingCount: this.followingCount, followersCount: this.followersCount, - avatar + avatar, + createdAt: this.createdAt, + updatedAt: this.updatedAt } } diff --git a/server/models/server/server.ts b/server/models/server/server.ts index d35aa0ca4..c43146156 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts @@ -1,8 +1,5 @@ -import * as Sequelize from 'sequelize' -import { AllowNull, Column, CreatedAt, Default, Is, IsInt, Max, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { isHostValid } from '../../helpers/custom-validators/servers' -import { logger } from '../../helpers/logger' -import { SERVERS_SCORE } from '../../initializers' import { throwIfNotValid } from '../utils' @Table({ @@ -11,9 +8,6 @@ import { throwIfNotValid } from '../utils' { fields: [ 'host' ], unique: true - }, - { - fields: [ 'score' ] } ] }) @@ -24,86 +18,9 @@ export class ServerModel extends Model { @Column host: string - @AllowNull(false) - @Default(SERVERS_SCORE.BASE) - @IsInt - @Max(SERVERS_SCORE.MAX) - @Column - score: number - @CreatedAt createdAt: Date @UpdatedAt updatedAt: Date - - static updateServersScoreAndRemoveBadOnes (goodServers: number[], badServers: number[]) { - logger.info('Updating %d good servers and %d bad servers scores.', goodServers.length, badServers.length) - - if (goodServers.length !== 0) { - ServerModel.incrementScores(goodServers, SERVERS_SCORE.BONUS) - .catch(err => { - logger.error('Cannot increment scores of good servers.', err) - }) - } - - if (badServers.length !== 0) { - ServerModel.incrementScores(badServers, SERVERS_SCORE.PENALTY) - .then(() => ServerModel.removeBadServers()) - .catch(err => { - if (err) logger.error('Cannot decrement scores of bad servers.', err) - }) - - } - } - - // Remove servers with a score of 0 (too many requests where they were unreachable) - private static async removeBadServers () { - try { - const servers = await ServerModel.listBadServers() - - const serversRemovePromises = servers.map(server => server.destroy()) - await Promise.all(serversRemovePromises) - - const numberOfServersRemoved = servers.length - - if (numberOfServersRemoved) { - logger.info('Removed %d servers.', numberOfServersRemoved) - } else { - logger.info('No need to remove bad servers.') - } - } catch (err) { - logger.error('Cannot remove bad servers.', err) - } - } - - private static incrementScores (ids: number[], value: number) { - const update = { - score: Sequelize.literal('score +' + value) - } - - const options = { - where: { - id: { - [Sequelize.Op.in]: ids - } - }, - // In this case score is a literal and not an integer so we do not validate it - validate: false - } - - return ServerModel.update(update, options) - } - - private static listBadServers () { - const query = { - where: { - score: { - [Sequelize.Op.lte]: 0 - } - } - } - - return ServerModel.findAll(query) - } } diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index acc2486b3..e2cbf0422 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -228,7 +228,7 @@ export class VideoChannelModel extends Model { const actor = this.Actor.toFormattedJSON() const account = { id: this.id, - name: this.name, + displayName: this.name, description: this.description, isLocal: this.Actor.isOwned(), createdAt: this.createdAt, diff --git a/shared/models/actors/account.model.ts b/shared/models/actors/account.model.ts index e4dbc81e5..5cc12c18f 100644 --- a/shared/models/actors/account.model.ts +++ b/shared/models/actors/account.model.ts @@ -1,15 +1,5 @@ -import { Avatar } from '../avatars/avatar.model' +import { Actor } from './actor.model' -export interface Account { - id: number - uuid: string - url: string - name: string +export interface Account extends Actor { displayName: string - host: string - followingCount: number - followersCount: number - createdAt: Date - updatedAt: Date - avatar: Avatar } diff --git a/shared/models/actors/actor.model.ts b/shared/models/actors/actor.model.ts new file mode 100644 index 000000000..f91616519 --- /dev/null +++ b/shared/models/actors/actor.model.ts @@ -0,0 +1,14 @@ +import { Avatar } from '../avatars/avatar.model' + +export interface Actor { + id: number + uuid: string + url: string + name: string + host: string + followingCount: number + followersCount: number + createdAt: Date + updatedAt: Date + avatar: Avatar +} diff --git a/shared/models/actors/follow.model.ts b/shared/models/actors/follow.model.ts index cdc3da560..70562bfc7 100644 --- a/shared/models/actors/follow.model.ts +++ b/shared/models/actors/follow.model.ts @@ -1,11 +1,12 @@ -import { Account } from './account.model' +import { Actor } from './actor.model' export type FollowState = 'pending' | 'accepted' export interface AccountFollow { id: number - follower: Account - following: Account + follower: Actor + following: Actor + score: number state: FollowState createdAt: Date updatedAt: Date diff --git a/shared/models/videos/video-channel.model.ts b/shared/models/videos/video-channel.model.ts index d1a952826..b164fb555 100644 --- a/shared/models/videos/video-channel.model.ts +++ b/shared/models/videos/video-channel.model.ts @@ -1,13 +1,10 @@ +import { Actor } from '../actors/actor.model' import { Video } from './video.model' -export interface VideoChannel { - id: number - name: string - url: string +export interface VideoChannel extends Actor { + displayName: string description: string isLocal: boolean - createdAt: Date | string - updatedAt: Date | string owner?: { name: string uuid: string