import * as Sequelize from 'sequelize'
-
-import {
- isUserUsernameValid,
- isAccountPublicKeyValid,
- isAccountUrlValid,
- isAccountPrivateKeyValid,
- isAccountFollowersCountValid,
- isAccountFollowingCountValid,
- isAccountInboxValid,
- isAccountOutboxValid,
- isAccountSharedInboxValid,
- isAccountFollowersValid,
- isAccountFollowingValid,
- activityPubContextify
-} from '../../helpers'
-
-import { addMethodsToModel, getSort } from '../utils'
import {
- AccountInstance,
- AccountAttributes,
-
- AccountMethods
-} from './account-interface'
-import LoadApplication = AccountMethods.LoadApplication
-import { sendDeleteAccount } from '../../lib/activitypub/send-request'
-
-let Account: Sequelize.Model<AccountInstance, AccountAttributes>
-let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
-let load: AccountMethods.Load
-let loadApplication: AccountMethods.LoadApplication
-let loadByUUID: AccountMethods.LoadByUUID
-let loadByUrl: AccountMethods.LoadByUrl
-let loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod
-let listOwned: AccountMethods.ListOwned
-let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
-let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
-let listFollowingForApi: AccountMethods.ListFollowingForApi
-let listFollowersForApi: AccountMethods.ListFollowersForApi
-let isOwned: AccountMethods.IsOwned
-let toActivityPubObject: AccountMethods.ToActivityPubObject
-let toFormattedJSON: AccountMethods.ToFormattedJSON
-let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
-let getFollowingUrl: AccountMethods.GetFollowingUrl
-let getFollowersUrl: AccountMethods.GetFollowersUrl
-let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
-
-export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
- Account = sequelize.define<AccountInstance, AccountAttributes>('Account',
- {
- uuid: {
- type: DataTypes.UUID,
- defaultValue: DataTypes.UUIDV4,
- allowNull: false,
- validate: {
- isUUID: 4
- }
- },
- name: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- usernameValid: value => {
- const res = isUserUsernameValid(value)
- if (res === false) throw new Error('Username is not valid.')
- }
- }
- },
- url: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- urlValid: value => {
- const res = isAccountUrlValid(value)
- if (res === false) throw new Error('URL is not valid.')
- }
- }
- },
- publicKey: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- publicKeyValid: value => {
- const res = isAccountPublicKeyValid(value)
- if (res === false) throw new Error('Public key is not valid.')
- }
- }
- },
- privateKey: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- privateKeyValid: value => {
- const res = isAccountPrivateKeyValid(value)
- if (res === false) throw new Error('Private key is not valid.')
- }
- }
- },
- followersCount: {
- type: DataTypes.INTEGER,
- allowNull: false,
- validate: {
- followersCountValid: value => {
- const res = isAccountFollowersCountValid(value)
- if (res === false) throw new Error('Followers count is not valid.')
- }
- }
- },
- followingCount: {
- type: DataTypes.INTEGER,
- allowNull: false,
- validate: {
- followersCountValid: value => {
- const res = isAccountFollowingCountValid(value)
- if (res === false) throw new Error('Following count is not valid.')
- }
- }
- },
- inboxUrl: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- inboxUrlValid: value => {
- const res = isAccountInboxValid(value)
- if (res === false) throw new Error('Inbox URL is not valid.')
- }
- }
- },
- outboxUrl: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- outboxUrlValid: value => {
- const res = isAccountOutboxValid(value)
- if (res === false) throw new Error('Outbox URL is not valid.')
- }
- }
- },
- sharedInboxUrl: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- sharedInboxUrlValid: value => {
- const res = isAccountSharedInboxValid(value)
- if (res === false) throw new Error('Shared inbox URL is not valid.')
- }
- }
- },
- followersUrl: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- followersUrlValid: value => {
- const res = isAccountFollowersValid(value)
- if (res === false) throw new Error('Followers URL is not valid.')
- }
- }
- },
- followingUrl: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- followingUrlValid: value => {
- const res = isAccountFollowingValid(value)
- if (res === false) throw new Error('Following URL is not valid.')
- }
- }
- }
- },
+ AllowNull,
+ BeforeDestroy,
+ BelongsTo,
+ Column,
+ CreatedAt,
+ Default,
+ DefaultScope,
+ ForeignKey,
+ HasMany,
+ Is,
+ Model,
+ Table,
+ UpdatedAt
+} from 'sequelize-typescript'
+import { Account } from '../../../shared/models/actors'
+import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts'
+import { logger } from '../../helpers/logger'
+import { sendDeleteActor } from '../../lib/activitypub/send'
+import { ActorModel } from '../activitypub/actor'
+import { ApplicationModel } from '../application/application'
+import { AvatarModel } from '../avatar/avatar'
+import { ServerModel } from '../server/server'
+import { getSort, throwIfNotValid } from '../utils'
+import { VideoChannelModel } from '../video/video-channel'
+import { VideoCommentModel } from '../video/video-comment'
+import { UserModel } from './user'
+
+@DefaultScope({
+ include: [
{
- indexes: [
- {
- fields: [ 'name' ]
- },
- {
- fields: [ 'podId' ]
- },
- {
- fields: [ 'userId' ],
- unique: true
- },
+ model: () => ActorModel,
+ required: true,
+ include: [
{
- fields: [ 'applicationId' ],
- unique: true
+ model: () => ServerModel,
+ required: false
},
{
- fields: [ 'name', 'podId' ],
- unique: true
+ model: () => AvatarModel,
+ required: false
}
- ],
- hooks: { afterDestroy }
+ ]
}
- )
-
- const classMethods = [
- associate,
- loadAccountByPodAndUUID,
- loadApplication,
- load,
- loadByUUID,
- loadLocalAccountByNameAndPod,
- listOwned,
- listFollowerUrlsForApi,
- listFollowingUrlsForApi,
- listFollowingForApi,
- listFollowersForApi
- ]
- const instanceMethods = [
- isOwned,
- toActivityPubObject,
- toFormattedJSON,
- getFollowerSharedInboxUrls,
- getFollowingUrl,
- getFollowersUrl,
- getPublicKeyUrl
]
- addMethodsToModel(Account, classMethods, instanceMethods)
+})
+@Table({
+ tableName: 'account'
+})
+export class AccountModel extends Model<AccountModel> {
- return Account
-}
+ @AllowNull(false)
+ @Column
+ name: string
+
+ @AllowNull(true)
+ @Default(null)
+ @Is('AccountDescription', value => throwIfNotValid(value, isAccountDescriptionValid, 'description'))
+ @Column
+ description: string
+
+ @CreatedAt
+ createdAt: Date
-// ---------------------------------------------------------------------------
+ @UpdatedAt
+ updatedAt: Date
-function associate (models) {
- Account.belongsTo(models.Pod, {
+ @ForeignKey(() => ActorModel)
+ @Column
+ actorId: number
+
+ @BelongsTo(() => ActorModel, {
foreignKey: {
- name: 'podId',
- allowNull: true
+ allowNull: false
},
onDelete: 'cascade'
})
+ Actor: ActorModel
+
+ @ForeignKey(() => UserModel)
+ @Column
+ userId: number
- Account.belongsTo(models.User, {
+ @BelongsTo(() => UserModel, {
foreignKey: {
- name: 'userId',
allowNull: true
},
onDelete: 'cascade'
})
+ User: UserModel
- Account.belongsTo(models.Application, {
+ @ForeignKey(() => ApplicationModel)
+ @Column
+ applicationId: number
+
+ @BelongsTo(() => ApplicationModel, {
foreignKey: {
- name: 'userId',
allowNull: true
},
onDelete: 'cascade'
})
+ Application: ApplicationModel
- Account.hasMany(models.VideoChannel, {
+ @HasMany(() => VideoChannelModel, {
foreignKey: {
- name: 'accountId',
allowNull: false
},
onDelete: 'cascade',
hooks: true
})
+ VideoChannels: VideoChannelModel[]
- Account.hasMany(models.AccountFollower, {
+ @HasMany(() => VideoCommentModel, {
foreignKey: {
- name: 'accountId',
allowNull: false
},
- as: 'following',
- onDelete: 'cascade'
- })
-
- Account.hasMany(models.AccountFollower, {
- foreignKey: {
- name: 'targetAccountId',
- allowNull: false
- },
- as: 'followers',
- onDelete: 'cascade'
+ onDelete: 'cascade',
+ hooks: true
})
-}
+ VideoComments: VideoCommentModel[]
-function afterDestroy (account: AccountInstance) {
- if (account.isOwned()) {
- return sendDeleteAccount(account, undefined)
- }
+ @BeforeDestroy
+ static async sendDeleteIfOwned (instance: AccountModel, options) {
+ if (!instance.Actor) {
+ instance.Actor = await instance.$get('Actor', { transaction: options.transaction }) as ActorModel
+ }
- return undefined
-}
+ if (instance.isOwned()) {
+ logger.debug('Sending delete of actor of account %s.', instance.Actor.url)
+ return sendDeleteActor(instance.Actor, options.transaction)
+ }
-toFormattedJSON = function (this: AccountInstance) {
- const json = {
- id: this.id,
- host: this.Pod.host,
- name: this.name
+ return undefined
}
- return json
-}
-
-toActivityPubObject = function (this: AccountInstance) {
- const type = this.podId ? 'Application' as 'Application' : 'Person' as 'Person'
-
- const json = {
- type,
- id: this.url,
- following: this.getFollowingUrl(),
- followers: this.getFollowersUrl(),
- inbox: this.inboxUrl,
- outbox: this.outboxUrl,
- preferredUsername: this.name,
- url: this.url,
- name: this.name,
- endpoints: {
- sharedInbox: this.sharedInboxUrl
- },
- uuid: this.uuid,
- publicKey: {
- id: this.getPublicKeyUrl(),
- owner: this.url,
- publicKeyPem: this.publicKey
- }
+ static load (id: number) {
+ return AccountModel.findById(id)
}
- return activityPubContextify(json)
-}
-
-isOwned = function (this: AccountInstance) {
- return this.podId === null
-}
-
-getFollowerSharedInboxUrls = function (this: AccountInstance) {
- const query: Sequelize.FindOptions<AccountAttributes> = {
- attributes: [ 'sharedInboxUrl' ],
- include: [
- {
- model: Account['sequelize'].models.AccountFollower,
- where: {
- targetAccountId: this.id
+ static loadByUUID (uuid: string) {
+ const query = {
+ include: [
+ {
+ model: ActorModel,
+ required: true,
+ where: {
+ uuid
+ }
}
- }
- ]
- }
-
- return Account.findAll(query)
- .then(accounts => accounts.map(a => a.sharedInboxUrl))
-}
-
-getFollowingUrl = function (this: AccountInstance) {
- return this.url + '/followers'
-}
-
-getFollowersUrl = function (this: AccountInstance) {
- return this.url + '/followers'
-}
-
-getPublicKeyUrl = function (this: AccountInstance) {
- return this.url + '#main-key'
-}
-
-// ------------------------------ STATICS ------------------------------
-
-listOwned = function () {
- const query: Sequelize.FindOptions<AccountAttributes> = {
- where: {
- podId: null
+ ]
}
- }
- return Account.findAll(query)
-}
-
-listFollowerUrlsForApi = function (id: number, start: number, count?: number) {
- return createListFollowForApiQuery('followers', id, start, count)
-}
-
-listFollowingUrlsForApi = function (id: number, start: number, count?: number) {
- return createListFollowForApiQuery('following', id, start, count)
-}
+ return AccountModel.findOne(query)
+ }
-listFollowingForApi = function (id: number, start: number, count: number, sort: string) {
- const query = {
- distinct: true,
- offset: start,
- limit: count,
- order: [ getSort(sort) ],
- include: [
- {
- model: Account['sequelize'].models.AccountFollow,
- required: true,
- as: 'following',
- include: [
+ static loadLocalByName (name: string) {
+ const query = {
+ where: {
+ [ Sequelize.Op.or ]: [
+ {
+ userId: {
+ [ Sequelize.Op.ne ]: null
+ }
+ },
{
- model: Account['sequelize'].models.Account,
- as: 'following',
- required: true,
- include: [ Account['sequelize'].models.Pod ]
+ applicationId: {
+ [ Sequelize.Op.ne ]: null
+ }
}
]
- }
- ]
- }
-
- return Account.findAndCountAll(query).then(({ rows, count }) => {
- return {
- data: rows,
- total: count
+ },
+ include: [
+ {
+ model: ActorModel,
+ required: true,
+ where: {
+ preferredUsername: name
+ }
+ }
+ ]
}
- })
-}
-listFollowersForApi = function (id: number, start: number, count: number, sort: string) {
- const query = {
- distinct: true,
- offset: start,
- limit: count,
- order: [ getSort(sort) ],
- include: [
- {
- model: Account['sequelize'].models.AccountFollow,
- required: true,
- as: 'followers',
- include: [
- {
- model: Account['sequelize'].models.Account,
- as: 'followers',
- required: true,
- include: [ Account['sequelize'].models.Pod ]
- }
- ]
- }
- ]
+ return AccountModel.findOne(query)
}
- return Account.findAndCountAll(query).then(({ rows, count }) => {
- return {
- data: rows,
- total: count
+ static loadLocalByNameAndHost (name: string, host: string) {
+ const query = {
+ include: [
+ {
+ model: ActorModel,
+ required: true,
+ where: {
+ preferredUsername: name
+ },
+ include: [
+ {
+ model: ServerModel,
+ required: true,
+ where: {
+ host
+ }
+ }
+ ]
+ }
+ ]
}
- })
-}
-loadApplication = function () {
- return Account.findOne({
- include: [
- {
- model: Account['sequelize'].model.Application,
- required: true
- }
- ]
- })
-}
-
-load = function (id: number) {
- return Account.findById(id)
-}
+ return AccountModel.findOne(query)
+ }
-loadByUUID = function (uuid: string) {
- const query: Sequelize.FindOptions<AccountAttributes> = {
- where: {
- uuid
+ static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
+ const query = {
+ include: [
+ {
+ model: ActorModel,
+ required: true,
+ where: {
+ url
+ }
+ }
+ ],
+ transaction
}
+
+ return AccountModel.findOne(query)
}
- return Account.findOne(query)
-}
+ static listForApi (start: number, count: number, sort: string) {
+ const query = {
+ offset: start,
+ limit: count,
+ order: getSort(sort)
+ }
-loadLocalAccountByNameAndPod = function (name: string, host: string) {
- const query: Sequelize.FindOptions<AccountAttributes> = {
- where: {
- name,
- userId: {
- [Sequelize.Op.ne]: null
- }
- },
- include: [
- {
- model: Account['sequelize'].models.Pod,
- where: {
- host
+ return AccountModel.findAndCountAll(query)
+ .then(({ rows, count }) => {
+ return {
+ data: rows,
+ total: count
}
- }
- ]
+ })
}
- return Account.findOne(query)
-}
-
-loadByUrl = function (url: string) {
- const query: Sequelize.FindOptions<AccountAttributes> = {
- where: {
- url
+ toFormattedJSON (): Account {
+ const actor = this.Actor.toFormattedJSON()
+ const account = {
+ id: this.id,
+ displayName: this.getDisplayName(),
+ description: this.description,
+ createdAt: this.createdAt,
+ updatedAt: this.updatedAt
}
- }
-
- return Account.findOne(query)
-}
-loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
- const query: Sequelize.FindOptions<AccountAttributes> = {
- where: {
- podId,
- uuid
- },
- transaction
+ return Object.assign(actor, account)
}
- return Account.find(query)
-}
-
-// ------------------------------ UTILS ------------------------------
+ toActivityPubObject () {
+ const obj = this.Actor.toActivityPubObject(this.name, 'Account')
-async function createListFollowForApiQuery (type: 'followers' | 'following', id: number, start: number, count?: number) {
- let firstJoin: string
- let secondJoin: string
-
- if (type === 'followers') {
- firstJoin = 'targetAccountId'
- secondJoin = 'accountId'
- } else {
- firstJoin = 'accountId'
- secondJoin = 'targetAccountId'
+ return Object.assign(obj, {
+ summary: this.description
+ })
}
- const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ]
- const tasks: Promise<any>[] = []
-
- for (const selection of selections) {
- let query = 'SELECT ' + selection + ' FROM "Account" ' +
- 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' +
- 'INNER JOIN "Account" AS "Follows" ON "Followers"."id" = "Follows"."' + secondJoin + '" ' +
- 'WHERE "Account"."id" = $id ' +
- 'LIMIT ' + start
-
- if (count !== undefined) query += ', ' + count
-
- const options = {
- bind: { id },
- type: Sequelize.QueryTypes.SELECT
- }
- tasks.push(Account['sequelize'].query(query, options))
+ isOwned () {
+ return this.Actor.isOwned()
}
- const [ followers, [ { total } ]] = await Promise.all(tasks)
- const urls: string[] = followers.map(f => f.url)
-
- return {
- data: urls,
- total: parseInt(total, 10)
+ getDisplayName () {
+ return this.name
}
}