@@ -99,7 +99,7 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons | |||
const obj = results.find(r => { | |||
const server = r.ActorFollowing.Server | |||
return r.ActorFollowing.preferredUsername === sanitizedHandle.name && | |||
return r.ActorFollowing.preferredUsername.toLowerCase() === sanitizedHandle.name.toLowerCase() && | |||
( | |||
(!server && !sanitizedHandle.host) || | |||
(server.host === sanitizedHandle.host) | |||
@@ -27,7 +27,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
// --------------------------------------------------------------------------- | |||
const LAST_MIGRATION_VERSION = 765 | |||
const LAST_MIGRATION_VERSION = 770 | |||
// --------------------------------------------------------------------------- | |||
@@ -0,0 +1,44 @@ | |||
import * as Sequelize from 'sequelize' | |||
async function up (utils: { | |||
transaction: Sequelize.Transaction | |||
queryInterface: Sequelize.QueryInterface | |||
sequelize: Sequelize.Sequelize | |||
db: any | |||
}): Promise<void> { | |||
const { transaction } = utils | |||
await utils.sequelize.query('drop index if exists "actor_preferred_username"', { transaction }) | |||
await utils.sequelize.query('drop index if exists "actor_preferred_username_server_id"', { transaction }) | |||
await utils.sequelize.query( | |||
'DELETE FROM "actor" v1 USING (' + | |||
'SELECT MIN(id) as id, lower("preferredUsername") AS "lowerPreferredUsername", "serverId" ' + | |||
'FROM "actor" ' + | |||
'GROUP BY "lowerPreferredUsername", "serverId" HAVING COUNT(*) > 1 AND "serverId" IS NOT NULL' + | |||
') v2 ' + | |||
'WHERE lower(v1."preferredUsername") = v2."lowerPreferredUsername" AND v1."serverId" = v2."serverId" AND v1.id <> v2.id', | |||
{ transaction } | |||
) | |||
await utils.sequelize.query( | |||
'DELETE FROM "actor" v1 USING (' + | |||
'SELECT MIN(id) as id, lower("preferredUsername") AS "lowerPreferredUsername", "serverId" ' + | |||
'FROM "actor" ' + | |||
'GROUP BY "lowerPreferredUsername", "serverId" HAVING COUNT(*) > 1 AND "serverId" IS NULL' + | |||
') v2 ' + | |||
'WHERE lower(v1."preferredUsername") = v2."lowerPreferredUsername" AND v1."serverId" IS NULL AND v1.id <> v2.id', | |||
{ transaction } | |||
) | |||
} | |||
async function down (utils: { | |||
queryInterface: Sequelize.QueryInterface | |||
transaction: Sequelize.Transaction | |||
}) { | |||
} | |||
export { | |||
up, | |||
down | |||
} |
@@ -189,8 +189,10 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV | |||
model: ActorModel.unscoped(), | |||
required: true, | |||
where: { | |||
preferredUsername: accountName, | |||
serverId: null | |||
[Op.and]: [ | |||
ActorModel.wherePreferredUsername(accountName), | |||
{ serverId: null } | |||
] | |||
} | |||
} | |||
] | |||
@@ -37,8 +37,8 @@ import { ActorImageModel } from '../actor/actor-image' | |||
import { ApplicationModel } from '../application/application' | |||
import { ServerModel } from '../server/server' | |||
import { ServerBlocklistModel } from '../server/server-blocklist' | |||
import { UserModel } from '../user/user' | |||
import { buildSQLAttributes, getSort, throwIfNotValid } from '../shared' | |||
import { UserModel } from '../user/user' | |||
import { VideoModel } from '../video/video' | |||
import { VideoChannelModel } from '../video/video-channel' | |||
import { VideoCommentModel } from '../video/video-comment' | |||
@@ -296,9 +296,7 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
{ | |||
model: ActorModel, | |||
required: true, | |||
where: { | |||
preferredUsername: name | |||
} | |||
where: ActorModel.wherePreferredUsername(name) | |||
} | |||
] | |||
} | |||
@@ -321,9 +319,7 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
{ | |||
model: ActorModel, | |||
required: true, | |||
where: { | |||
preferredUsername: name | |||
}, | |||
where: ActorModel.wherePreferredUsername(name), | |||
include: [ | |||
{ | |||
model: ServerModel, | |||
@@ -37,8 +37,8 @@ import { logger } from '../../helpers/logger' | |||
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' | |||
import { buildSQLAttributes, createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../shared' | |||
import { doesExist } from '../shared/query' | |||
import { VideoChannelModel } from '../video/video-channel' | |||
import { ActorModel, unusedActorAttributesForAPI } from './actor' | |||
import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' | |||
@@ -265,9 +265,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
model: ActorModel, | |||
required: true, | |||
as: 'ActorFollowing', | |||
where: { | |||
preferredUsername: targetName | |||
}, | |||
where: ActorModel.wherePreferredUsername(targetName), | |||
include: [ | |||
{ | |||
model: VideoChannelModel.unscoped(), | |||
@@ -313,24 +311,16 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
if (t.host) { | |||
return { | |||
[Op.and]: [ | |||
{ | |||
$preferredUsername$: t.name | |||
}, | |||
{ | |||
$host$: t.host | |||
} | |||
ActorModel.wherePreferredUsername(t.name, '$preferredUsername$'), | |||
{ $host$: t.host } | |||
] | |||
} | |||
} | |||
return { | |||
[Op.and]: [ | |||
{ | |||
$preferredUsername$: t.name | |||
}, | |||
{ | |||
$serverId$: null | |||
} | |||
ActorModel.wherePreferredUsername(t.name, '$preferredUsername$'), | |||
{ $serverId$: null } | |||
] | |||
} | |||
}) | |||
@@ -1,4 +1,4 @@ | |||
import { literal, Op, QueryTypes, Transaction } from 'sequelize' | |||
import { col, fn, literal, Op, QueryTypes, Transaction, where } from 'sequelize' | |||
import { | |||
AllowNull, | |||
BelongsTo, | |||
@@ -130,7 +130,8 @@ export const unusedActorAttributesForAPI: (keyof AttributesOnly<ActorModel>)[] = | |||
unique: true | |||
}, | |||
{ | |||
fields: [ 'preferredUsername', 'serverId' ], | |||
fields: [ fn('lower', col('preferredUsername')), 'serverId' ], | |||
name: 'actor_preferred_username_lower_server_id', | |||
unique: true, | |||
where: { | |||
serverId: { | |||
@@ -139,7 +140,8 @@ export const unusedActorAttributesForAPI: (keyof AttributesOnly<ActorModel>)[] = | |||
} | |||
}, | |||
{ | |||
fields: [ 'preferredUsername' ], | |||
fields: [ fn('lower', col('preferredUsername')) ], | |||
name: 'actor_preferred_username_lower', | |||
unique: true, | |||
where: { | |||
serverId: null | |||
@@ -327,6 +329,12 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
// --------------------------------------------------------------------------- | |||
static wherePreferredUsername (preferredUsername: string, colName = 'preferredUsername') { | |||
return where(fn('lower', col(colName)), preferredUsername.toLowerCase()) | |||
} | |||
// --------------------------------------------------------------------------- | |||
static async load (id: number): Promise<MActor> { | |||
const actorServer = await getServerActor() | |||
if (id === actorServer.id) return actorServer | |||
@@ -372,8 +380,12 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
const fun = () => { | |||
const query = { | |||
where: { | |||
preferredUsername, | |||
serverId: null | |||
[Op.and]: [ | |||
this.wherePreferredUsername(preferredUsername), | |||
{ | |||
serverId: null | |||
} | |||
] | |||
}, | |||
transaction | |||
} | |||
@@ -395,8 +407,12 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
const query = { | |||
attributes: [ 'url' ], | |||
where: { | |||
preferredUsername, | |||
serverId: null | |||
[Op.and]: [ | |||
this.wherePreferredUsername(preferredUsername), | |||
{ | |||
serverId: null | |||
} | |||
] | |||
}, | |||
transaction | |||
} | |||
@@ -405,7 +421,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
} | |||
return ModelCache.Instance.doCache({ | |||
cacheType: 'local-actor-name', | |||
cacheType: 'local-actor-url', | |||
key: preferredUsername, | |||
// The server actor never change, so we can easily cache it | |||
whitelist: () => preferredUsername === SERVER_ACTOR_NAME, | |||
@@ -415,9 +431,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
static loadByNameAndHost (preferredUsername: string, host: string): Promise<MActorFull> { | |||
const query = { | |||
where: { | |||
preferredUsername | |||
}, | |||
where: this.wherePreferredUsername(preferredUsername), | |||
include: [ | |||
{ | |||
model: ServerModel, | |||
@@ -130,13 +130,16 @@ export type SummaryOptions = { | |||
for (const handle of options.handles || []) { | |||
const [ preferredUsername, host ] = handle.split('@') | |||
const sanitizedPreferredUsername = VideoChannelModel.sequelize.escape(preferredUsername.toLowerCase()) | |||
const sanitizedHost = VideoChannelModel.sequelize.escape(host) | |||
if (!host || host === WEBSERVER.HOST) { | |||
or.push(`("preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} AND "serverId" IS NULL)`) | |||
or.push(`(LOWER("preferredUsername") = ${sanitizedPreferredUsername} AND "serverId" IS NULL)`) | |||
} else { | |||
or.push( | |||
`(` + | |||
`"preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} ` + | |||
`AND "host" = ${VideoChannelModel.sequelize.escape(host)}` + | |||
`LOWER("preferredUsername") = ${sanitizedPreferredUsername} ` + | |||
`AND "host" = ${sanitizedHost}` + | |||
`)` | |||
) | |||
} | |||
@@ -698,8 +701,10 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel | |||
model: ActorModel, | |||
required: true, | |||
where: { | |||
preferredUsername: name, | |||
serverId: null | |||
[Op.and]: [ | |||
ActorModel.wherePreferredUsername(name), | |||
{ serverId: null } | |||
] | |||
}, | |||
include: [ | |||
{ | |||
@@ -723,9 +728,7 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel | |||
{ | |||
model: ActorModel, | |||
required: true, | |||
where: { | |||
preferredUsername: name | |||
}, | |||
where: ActorModel.wherePreferredUsername(name), | |||
include: [ | |||
{ | |||
model: ServerModel, | |||