From f7cc67b455a12ccae9b0ea16876d166720364357 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 4 Jan 2019 08:56:20 +0100 Subject: Add new follow, mention and user registered notifs --- server/models/account/account-blocklist.ts | 24 +++++- server/models/account/user-notification-setting.ts | 32 +++++++- server/models/account/user-notification.ts | 95 ++++++++++++++++++++-- server/models/account/user.ts | 51 ++++++++++++ server/models/video/video-comment.ts | 41 +++++++++- 5 files changed, 228 insertions(+), 15 deletions(-) (limited to 'server/models') diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 54ac290c4..efd6ed59e 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts @@ -2,6 +2,7 @@ import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, Updated import { AccountModel } from './account' import { getSort } from '../utils' import { AccountBlock } from '../../../shared/models/blocklist' +import { Op } from 'sequelize' enum ScopeNames { WITH_ACCOUNTS = 'WITH_ACCOUNTS' @@ -73,18 +74,33 @@ export class AccountBlocklistModel extends Model { BlockedAccount: AccountModel static isAccountMutedBy (accountId: number, targetAccountId: number) { + return AccountBlocklistModel.isAccountMutedByMulti([ accountId ], targetAccountId) + .then(result => result[accountId]) + } + + static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) { const query = { - attributes: [ 'id' ], + attributes: [ 'accountId', 'id' ], where: { - accountId, + accountId: { + [Op.any]: accountIds + }, targetAccountId }, raw: true } return AccountBlocklistModel.unscoped() - .findOne(query) - .then(a => !!a) + .findAll(query) + .then(rows => { + const result: { [accountId: number]: boolean } = {} + + for (const accountId of accountIds) { + result[accountId] = !!rows.find(r => r.accountId === accountId) + } + + return result + }) } static loadByAccountAndTarget (accountId: number, targetAccountId: number) { diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index 6470defa7..f1c3ac223 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts @@ -83,6 +83,33 @@ export class UserNotificationSettingModel extends Model throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration') + ) + @Column + newUserRegistration: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewFollow', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') + ) + @Column + newFollow: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingCommentMention', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention') + ) + @Column + commentMention: UserNotificationSettingValue + @ForeignKey(() => UserModel) @Column userId: number @@ -114,7 +141,10 @@ export class UserNotificationSettingModel extends Model VideoChannelModel.unscoped() } } -function buildAccountInclude () { +function buildAccountInclude (required: boolean) { return { - required: true, + required, attributes: [ 'id', 'name' ], model: () => AccountModel.unscoped() } @@ -58,14 +60,14 @@ function buildAccountInclude () { [ScopeNames.WITH_ALL]: { include: [ Object.assign(buildVideoInclude(false), { - include: [ buildChannelInclude() ] + include: [ buildChannelInclude(true) ] }), { attributes: [ 'id', 'originCommentId' ], model: () => VideoCommentModel.unscoped(), required: false, include: [ - buildAccountInclude(), + buildAccountInclude(true), buildVideoInclude(true) ] }, @@ -86,6 +88,42 @@ function buildAccountInclude () { model: () => VideoImportModel.unscoped(), required: false, include: [ buildVideoInclude(false) ] + }, + { + attributes: [ 'id', 'name' ], + model: () => AccountModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'id', 'preferredUsername' ], + model: () => ActorModel.unscoped(), + required: true + } + ] + }, + { + attributes: [ 'id' ], + model: () => ActorFollowModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'preferredUsername' ], + model: () => ActorModel.unscoped(), + required: true, + as: 'ActorFollower', + include: [ buildAccountInclude(true) ] + }, + { + attributes: [ 'preferredUsername' ], + model: () => ActorModel.unscoped(), + required: true, + as: 'ActorFollowing', + include: [ + buildChannelInclude(false), + buildAccountInclude(false) + ] + } + ] } ] } @@ -193,6 +231,30 @@ export class UserNotificationModel extends Model { }) VideoImport: VideoImportModel + @ForeignKey(() => AccountModel) + @Column + accountId: number + + @BelongsTo(() => AccountModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Account: AccountModel + + @ForeignKey(() => ActorFollowModel) + @Column + actorFollowId: number + + @BelongsTo(() => ActorFollowModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + ActorFollow: ActorFollowModel + static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { const query: IFindOptions = { offset: start, @@ -264,6 +326,25 @@ export class UserNotificationModel extends Model { video: this.formatVideo(this.VideoBlacklist.Video) } : undefined + const account = this.Account ? { + id: this.Account.id, + displayName: this.Account.getDisplayName(), + name: this.Account.Actor.preferredUsername + } : undefined + + const actorFollow = this.ActorFollow ? { + id: this.ActorFollow.id, + follower: { + displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), + name: this.ActorFollow.ActorFollower.preferredUsername + }, + following: { + type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', + displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(), + name: this.ActorFollow.ActorFollowing.preferredUsername + } + } : undefined + return { id: this.id, type: this.type, @@ -273,6 +354,8 @@ export class UserNotificationModel extends Model { comment, videoAbuse, videoBlacklist, + account, + actorFollow, createdAt: this.createdAt.toISOString(), updatedAt: this.updatedAt.toISOString() } diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 33f56f641..017a96657 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -330,6 +330,16 @@ export class UserModel extends Model { return UserModel.unscoped().findAll(query) } + static listByUsernames (usernames: string[]) { + const query = { + where: { + username: usernames + } + } + + return UserModel.findAll(query) + } + static loadById (id: number) { return UserModel.findById(id) } @@ -424,6 +434,47 @@ export class UserModel extends Model { return UserModel.findOne(query) } + static loadByChannelActorId (videoChannelActorId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + where: { + actorId: videoChannelActorId + } + } + ] + } + ] + } + + return UserModel.findOne(query) + } + + static loadByAccountActorId (accountActorId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + where: { + actorId: accountActorId + } + } + ] + } + + return UserModel.findOne(query) + } + static getOriginalVideoFileTotalFromUser (user: UserModel) { // Don't use sequelize because we need to use a sub query const query = UserModel.generateUserQuotaBaseSQL() diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index d8fc2a564..cf6278da7 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -18,7 +18,7 @@ import { ActivityTagObject } from '../../../shared/models/activitypub/objects/co import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' import { VideoComment } from '../../../shared/models/videos/video-comment.model' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { CONSTRAINTS_FIELDS } from '../../initializers' +import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' import { sendDeleteVideoComment } from '../../lib/activitypub/send' import { AccountModel } from '../account/account' import { ActorModel } from '../activitypub/actor' @@ -29,6 +29,9 @@ import { VideoModel } from './video' import { VideoChannelModel } from './video-channel' import { getServerActor } from '../../helpers/utils' import { UserModel } from '../account/user' +import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' +import { regexpCapture } from '../../helpers/regexp' +import { uniq } from 'lodash' enum ScopeNames { WITH_ACCOUNT = 'WITH_ACCOUNT', @@ -370,9 +373,11 @@ export class VideoCommentModel extends Model { id: { [ Sequelize.Op.in ]: Sequelize.literal('(' + 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + - 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' + - 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' + - 'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' + + `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + + 'UNION ' + + 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' + + 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' + + ') ' + 'SELECT id FROM children' + ')'), [ Sequelize.Op.ne ]: comment.id @@ -460,6 +465,34 @@ export class VideoCommentModel extends Model { return this.Account.isOwned() } + extractMentions () { + if (!this.text) return [] + + const localMention = `@(${actorNameAlphabet}+)` + const remoteMention = `${localMention}@${CONFIG.WEBSERVER.HOST}` + + const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g') + const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g') + const firstMentionRegex = new RegExp('^(?:(?:' + remoteMention + ')|(?:' + localMention + ')) ', 'g') + const endMentionRegex = new RegExp(' (?:(?:' + remoteMention + ')|(?:' + localMention + '))$', 'g') + + return uniq( + [].concat( + regexpCapture(this.text, remoteMentionsRegex) + .map(([ , username ]) => username), + + regexpCapture(this.text, localMentionsRegex) + .map(([ , username ]) => username), + + regexpCapture(this.text, firstMentionRegex) + .map(([ , username1, username2 ]) => username1 || username2), + + regexpCapture(this.text, endMentionRegex) + .map(([ , username1, username2 ]) => username1 || username2) + ) + ) + } + toFormattedJSON () { return { id: this.id, -- cgit v1.2.3