Merge branch 'refactor/system-user' into 'develop'

fix: correct user count


See merge request firefish/firefish!11124
This commit is contained in:
naskya 2024-07-10 15:26:10 +00:00
commit 277bffa08a
15 changed files with 68 additions and 54 deletions

View file

@ -1,6 +1,7 @@
BEGIN; BEGIN;
DELETE FROM "migrations" WHERE name IN ( DELETE FROM "migrations" WHERE name IN (
'CreateSystemActors1720618854585',
'AddMastodonSubscriptionType1715181461692', 'AddMastodonSubscriptionType1715181461692',
'SwSubscriptionAccessToken1709395223611', 'SwSubscriptionAccessToken1709395223611',
'UserProfileMentions1711075007936', 'UserProfileMentions1711075007936',

View file

@ -257,6 +257,8 @@ export interface Config {
userAgent: string userAgent: string
} }
export declare function countLocalUsers(): Promise<number>
export declare function countReactions(reactions: Record<string, number>): Record<string, number> export declare function countReactions(reactions: Record<string, number>): Record<string, number>
export interface Cpu { export interface Cpu {

View file

@ -366,6 +366,7 @@ module.exports.AntennaSrc = nativeBinding.AntennaSrc
module.exports.ChatEvent = nativeBinding.ChatEvent module.exports.ChatEvent = nativeBinding.ChatEvent
module.exports.ChatIndexEvent = nativeBinding.ChatIndexEvent module.exports.ChatIndexEvent = nativeBinding.ChatIndexEvent
module.exports.checkWordMute = nativeBinding.checkWordMute module.exports.checkWordMute = nativeBinding.checkWordMute
module.exports.countLocalUsers = nativeBinding.countLocalUsers
module.exports.countReactions = nativeBinding.countReactions module.exports.countReactions = nativeBinding.countReactions
module.exports.cpuInfo = nativeBinding.cpuInfo module.exports.cpuInfo = nativeBinding.cpuInfo
module.exports.cpuUsage = nativeBinding.cpuUsage module.exports.cpuUsage = nativeBinding.cpuUsage

View file

@ -4,6 +4,7 @@ use crate::{
config::{local_server_info, CONFIG}, config::{local_server_info, CONFIG},
database::db_conn, database::db_conn,
federation::nodeinfo::schema::*, federation::nodeinfo::schema::*,
misc,
model::entity::{note, user}, model::entity::{note, user},
}; };
use sea_orm::prelude::*; use sea_orm::prelude::*;
@ -33,9 +34,7 @@ async fn statistics() -> Result<(u64, u64, u64, u64), DbErr> {
const MONTH: chrono::TimeDelta = chrono::Duration::days(30); const MONTH: chrono::TimeDelta = chrono::Duration::days(30);
const HALF_YEAR: chrono::TimeDelta = chrono::Duration::days(183); const HALF_YEAR: chrono::TimeDelta = chrono::Duration::days(183);
let local_users = user::Entity::find() let local_users = misc::user::count::local_total();
.filter(user::Column::Host.is_null())
.count(db);
let local_active_halfyear = user::Entity::find() let local_active_halfyear = user::Entity::find()
.filter(user::Column::Host.is_null()) .filter(user::Column::Host.is_null())
.filter(user::Column::LastActiveDate.gt(now - HALF_YEAR)) .filter(user::Column::LastActiveDate.gt(now - HALF_YEAR))

View file

@ -17,3 +17,4 @@ pub mod reaction;
pub mod remove_old_attestation_challenges; pub mod remove_old_attestation_challenges;
pub mod should_nyaify; pub mod should_nyaify;
pub mod system_info; pub mod system_info;
pub mod user;

View file

@ -0,0 +1,18 @@
use crate::{database::db_conn, model::entity::user};
use sea_orm::prelude::*;
// @instance.actor and @relay.actor are not real users
const NUMBER_OF_SYSTEM_ACTORS: u64 = 2;
pub async fn local_total() -> Result<u64, DbErr> {
user::Entity::find()
.filter(user::Column::Host.is_null())
.count(db_conn().await?)
.await
.map(|count| count - NUMBER_OF_SYSTEM_ACTORS)
}
#[macros::ts_export(js_name = "countLocalUsers")]
pub async fn local_total_js() -> Result<u32, DbErr> {
local_total().await.map(|count| count as u32)
}

View file

@ -0,0 +1 @@
pub mod count;

View file

@ -1,3 +1,5 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { genRsaKeyPair } from "@/misc/gen-key-pair.js"; import { genRsaKeyPair } from "@/misc/gen-key-pair.js";
import { User } from "@/models/entities/user.js"; import { User } from "@/models/entities/user.js";
@ -9,7 +11,7 @@ import { UserKeypair } from "@/models/entities/user-keypair.js";
import { UsedUsername } from "@/models/entities/used-username.js"; import { UsedUsername } from "@/models/entities/used-username.js";
import { db } from "@/db/postgre.js"; import { db } from "@/db/postgre.js";
export async function createSystemUser(username: string) { async function createSystemUser(username: string) {
const password = uuid(); const password = uuid();
// Generate hash of password // Generate hash of password
@ -20,22 +22,20 @@ export async function createSystemUser(username: string) {
const keyPair = await genRsaKeyPair(4096); const keyPair = await genRsaKeyPair(4096);
let account!: User;
const exists = await Users.existsBy({ const exists = await Users.existsBy({
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: IsNull(), host: IsNull(),
}); });
if (exists) { if (exists) {
throw new Error("the user already exists"); return;
} }
const now = new Date(); const now = new Date();
// Start transaction // Start transaction
await db.transaction(async (transactionalEntityManager) => { await db.transaction(async (transactionalEntityManager) => {
account = await transactionalEntityManager const account = await transactionalEntityManager
.insert(User, { .insert(User, {
id: genIdAt(now), id: genIdAt(now),
createdAt: now, createdAt: now,
@ -69,6 +69,18 @@ export async function createSystemUser(username: string) {
username: username.toLowerCase(), username: username.toLowerCase(),
}); });
}); });
}
return account;
export class CreateSystemActors1720618854585 implements MigrationInterface {
public async up(_: QueryRunner): Promise<void> {
if (!db.isInitialized) {
db.initialize();
}
await createSystemUser("instance.actor");
await createSystemUser("relay.actor");
}
public async down(_: QueryRunner): Promise<void> {
/* You don't need to revert this migration. */
}
} }

View file

@ -3,7 +3,13 @@ import { User } from "@/models/entities/user.js";
import { Users, UsedUsernames } from "@/models/index.js"; import { Users, UsedUsernames } from "@/models/index.js";
import { UserProfile } from "@/models/entities/user-profile.js"; import { UserProfile } from "@/models/entities/user-profile.js";
import { IsNull } from "typeorm"; import { IsNull } from "typeorm";
import { genIdAt, generateUserToken, hashPassword, toPuny } from "backend-rs"; import {
countLocalUsers,
genIdAt,
generateUserToken,
hashPassword,
toPuny,
} from "backend-rs";
import { UserKeypair } from "@/models/entities/user-keypair.js"; import { UserKeypair } from "@/models/entities/user-keypair.js";
import { UsedUsername } from "@/models/entities/used-username.js"; import { UsedUsername } from "@/models/entities/used-username.js";
import { db } from "@/db/postgre.js"; import { db } from "@/db/postgre.js";
@ -18,9 +24,7 @@ export async function signup(opts: {
const { username, password, passwordHash, host } = opts; const { username, password, passwordHash, host } = opts;
let hash = passwordHash; let hash = passwordHash;
const userCount = await Users.countBy({ const userCount = await countLocalUsers();
host: IsNull(),
});
if (config.maxUserSignups != null && userCount > config.maxUserSignups) { if (config.maxUserSignups != null && userCount > config.maxUserSignups) {
throw new Error("MAX_USERS_REACHED"); throw new Error("MAX_USERS_REACHED");
@ -103,11 +107,7 @@ export async function signup(opts: {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: host == null ? null : toPuny(host), host: host == null ? null : toPuny(host),
token: secret, token: secret,
isAdmin: isAdmin: (await countLocalUsers()) === 0,
(await Users.countBy({
host: IsNull(),
isAdmin: true,
})) === 0,
}), }),
); );

View file

@ -1,7 +1,7 @@
import define from "@/server/api/define.js"; import define from "@/server/api/define.js";
import { Users } from "@/models/index.js"; import { Users } from "@/models/index.js";
import { signup } from "@/server/api/common/signup.js"; import { signup } from "@/server/api/common/signup.js";
import { IsNull } from "typeorm"; import { countLocalUsers } from "backend-rs";
export const meta = { export const meta = {
tags: ["admin"], tags: ["admin"],
@ -32,11 +32,8 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, _me, token) => { export default define(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null; const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null;
const noUsers = if (!me?.isAdmin && (await countLocalUsers()) !== 0)
(await Users.countBy({ throw new Error("access denied");
host: IsNull(),
})) === 0;
if (!noUsers && !me?.isAdmin) throw new Error("access denied");
if (token) throw new Error("access denied"); if (token) throw new Error("access denied");
const { account, secret } = await signup({ const { account, secret } = await signup({

View file

@ -1,7 +1,7 @@
import JSON5 from "json5"; import JSON5 from "json5";
import { IsNull, MoreThan } from "typeorm"; import { IsNull, MoreThan } from "typeorm";
import { config } from "@/config.js"; import { config } from "@/config.js";
import { fetchMeta } from "backend-rs"; import { countLocalUsers, fetchMeta } from "backend-rs";
import { Ads, Emojis, Users } from "@/models/index.js"; import { Ads, Emojis, Users } from "@/models/index.js";
import define from "@/server/api/define.js"; import define from "@/server/api/define.js";
@ -501,11 +501,7 @@ export default define(meta, paramDef, async (ps, me) => {
instance.privateMode && !me ? [] : instance.pinnedClipId, instance.privateMode && !me ? [] : instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteFiles: instance.cacheRemoteFiles,
markLocalFilesNsfwByDefault: instance.markLocalFilesNsfwByDefault, markLocalFilesNsfwByDefault: instance.markLocalFilesNsfwByDefault,
requireSetup: requireSetup: (await countLocalUsers()) === 0,
(await Users.countBy({
host: IsNull(),
isAdmin: true,
})) === 0,
} }
: {}), : {}),
}; };

View file

@ -1,6 +1,7 @@
import { Instances, Users, Notes } from "@/models/index.js"; import { Instances, Users, Notes } from "@/models/index.js";
import define from "@/server/api/define.js"; import define from "@/server/api/define.js";
import { IsNull } from "typeorm"; import { IsNull } from "typeorm";
import { countLocalUsers } from "backend-rs";
export const meta = { export const meta = {
requireCredential: false, requireCredential: false,
@ -69,11 +70,7 @@ export default define(meta, paramDef, async () => {
// usersCount // usersCount
Users.count(), Users.count(),
// originalUsersCount // originalUsersCount
Users.count({ countLocalUsers(),
where: {
host: IsNull(),
},
}),
// instances // instances
Instances.count(), Instances.count(),
]); ]);

View file

@ -1,6 +1,6 @@
import { config } from "@/config.js"; import { config } from "@/config.js";
import { FILE_TYPE_BROWSERSAFE } from "backend-rs"; import { FILE_TYPE_BROWSERSAFE } from "backend-rs";
import { fetchMeta } from "backend-rs"; import { countLocalUsers, fetchMeta } from "backend-rs";
import { import {
AnnouncementReads, AnnouncementReads,
Announcements, Announcements,
@ -31,7 +31,7 @@ export class MiscHelpers {
public static async getInstance( public static async getInstance(
ctx: MastoContext, ctx: MastoContext,
): Promise<MastodonEntity.Instance> { ): Promise<MastodonEntity.Instance> {
const userCount = Users.count({ where: { host: IsNull() } }); const userCount = countLocalUsers();
const noteCount = Notes.count({ where: { userHost: IsNull() } }); const noteCount = Notes.count({ where: { userHost: IsNull() } });
const instanceCount = Instances.count({ cache: 3600000 }); const instanceCount = Instances.count({ cache: 3600000 });
const contact = await Users.findOne({ const contact = await Users.findOne({
@ -109,7 +109,7 @@ export class MiscHelpers {
public static async getInstanceV2( public static async getInstanceV2(
ctx: MastoContext, ctx: MastoContext,
): Promise<MastodonEntity.InstanceV2> { ): Promise<MastodonEntity.InstanceV2> {
const userCount = await Users.count({ where: { host: IsNull() } }); const userCount = await countLocalUsers();
const contact = await Users.findOne({ const contact = await Users.findOne({
where: { where: {
host: IsNull(), host: IsNull(),

View file

@ -1,4 +1,3 @@
import { createSystemUser } from "./create-system-user.js";
import type { ILocalUser } from "@/models/entities/user.js"; import type { ILocalUser } from "@/models/entities/user.js";
import { Users } from "@/models/index.js"; import { Users } from "@/models/index.js";
import { Cache } from "@/misc/cache.js"; import { Cache } from "@/misc/cache.js";
@ -15,14 +14,8 @@ export async function getInstanceActor(): Promise<ILocalUser> {
const user = (await Users.findOneBy({ const user = (await Users.findOneBy({
host: IsNull(), host: IsNull(),
username: ACTOR_USERNAME, username: ACTOR_USERNAME,
})) as ILocalUser | undefined; })) as ILocalUser;
if (user) {
await cache.set(null, user); await cache.set(null, user);
return user; return user;
} else {
const created = (await createSystemUser(ACTOR_USERNAME)) as ILocalUser;
await cache.set(null, created);
return created;
}
} }

View file

@ -11,7 +11,6 @@ import { Users, Relays } from "@/models/index.js";
import { genId } from "backend-rs"; import { genId } from "backend-rs";
import { Cache } from "@/misc/cache.js"; import { Cache } from "@/misc/cache.js";
import type { Relay } from "@/models/entities/relay.js"; import type { Relay } from "@/models/entities/relay.js";
import { createSystemUser } from "@/services/create-system-user.js";
const ACTOR_USERNAME = "relay.actor" as const; const ACTOR_USERNAME = "relay.actor" as const;
@ -23,10 +22,7 @@ export async function getRelayActor(): Promise<ILocalUser> {
username: ACTOR_USERNAME, username: ACTOR_USERNAME,
}); });
if (user) return user as ILocalUser; return user as ILocalUser;
const created = await createSystemUser(ACTOR_USERNAME);
return created as ILocalUser;
} }
export async function addRelay(inbox: string) { export async function addRelay(inbox: string) {