refactoring

Resolve #7779
This commit is contained in:
syuilo
2021-11-12 02:02:25 +09:00
parent 037837b551
commit 0e4a111f81
1714 changed files with 20803 additions and 11751 deletions

View File

@ -0,0 +1,74 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class AbuseUserReport {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the AbuseUserReport.'
})
public createdAt: Date;
@Index()
@Column(id())
public targetUserId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public targetUser: User | null;
@Index()
@Column(id())
public reporterId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public reporter: User | null;
@Column({
...id(),
nullable: true
})
public assigneeId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'SET NULL'
})
@JoinColumn()
public assignee: User | null;
@Index()
@Column('boolean', {
default: false
})
public resolved: boolean;
@Column('varchar', {
length: 2048,
})
public comment: string;
//#region Denormalized fields
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public targetUserHost: string | null;
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public reporterHost: string | null;
//#endregion
}

View File

@ -0,0 +1,96 @@
import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user';
import { App } from './app';
import { id } from '../id';
@Entity()
export class AccessToken {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the AccessToken.'
})
public createdAt: Date;
@Column('timestamp with time zone', {
nullable: true,
default: null,
})
public lastUsedAt: Date | null;
@Index()
@Column('varchar', {
length: 128
})
public token: string;
@Index()
@Column('varchar', {
length: 128,
nullable: true,
default: null
})
public session: string | null;
@Index()
@Column('varchar', {
length: 128
})
public hash: string;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column({
...id(),
nullable: true,
default: null
})
public appId: App['id'] | null;
@ManyToOne(type => App, {
onDelete: 'CASCADE'
})
@JoinColumn()
public app: App | null;
@Column('varchar', {
length: 128,
nullable: true,
default: null
})
public name: string | null;
@Column('varchar', {
length: 512,
nullable: true,
default: null
})
public description: string | null;
@Column('varchar', {
length: 512,
nullable: true,
default: null
})
public iconUrl: string | null;
@Column('varchar', {
length: 64, array: true,
default: '{}'
})
public permission: string[];
@Column('boolean', {
default: false
})
public fetched: boolean;
}

View File

@ -0,0 +1,59 @@
import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
import { id } from '../id';
@Entity()
export class Ad {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Ad.'
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
comment: 'The expired date of the Ad.'
})
public expiresAt: Date;
@Column('varchar', {
length: 32, nullable: false
})
public place: string;
// 今は使われていないが将来的に活用される可能性はある
@Column('varchar', {
length: 32, nullable: false
})
public priority: string;
@Column('integer', {
default: 1, nullable: false
})
public ratio: number;
@Column('varchar', {
length: 1024, nullable: false
})
public url: string;
@Column('varchar', {
length: 1024, nullable: false
})
public imageUrl: string;
@Column('varchar', {
length: 8192, nullable: false
})
public memo: string;
constructor(data: Partial<Ad>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,36 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { Announcement } from './announcement';
import { id } from '../id';
@Entity()
@Index(['userId', 'announcementId'], { unique: true })
export class AnnouncementRead {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the AnnouncementRead.'
})
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column(id())
public announcementId: Announcement['id'];
@ManyToOne(type => Announcement, {
onDelete: 'CASCADE'
})
@JoinColumn()
public announcement: Announcement | null;
}

View File

@ -0,0 +1,43 @@
import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
import { id } from '../id';
@Entity()
export class Announcement {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Announcement.'
})
public createdAt: Date;
@Column('timestamp with time zone', {
comment: 'The updated date of the Announcement.',
nullable: true
})
public updatedAt: Date | null;
@Column('varchar', {
length: 8192, nullable: false
})
public text: string;
@Column('varchar', {
length: 256, nullable: false
})
public title: string;
@Column('varchar', {
length: 1024, nullable: true
})
public imageUrl: string | null;
constructor(data: Partial<Announcement>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,43 @@
import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
import { Note } from './note';
import { Antenna } from './antenna';
import { id } from '../id';
@Entity()
@Index(['noteId', 'antennaId'], { unique: true })
export class AntennaNote {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
comment: 'The note ID.'
})
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
@Index()
@Column({
...id(),
comment: 'The antenna ID.'
})
public antennaId: Antenna['id'];
@ManyToOne(type => Antenna, {
onDelete: 'CASCADE'
})
@JoinColumn()
public antenna: Antenna | null;
@Index()
@Column('boolean', {
default: false
})
public read: boolean;
}

View File

@ -0,0 +1,99 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { UserList } from './user-list';
import { UserGroupJoining } from './user-group-joining';
@Entity()
export class Antenna {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the Antenna.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The owner ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128,
comment: 'The name of the Antenna.'
})
public name: string;
@Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] })
public src: 'home' | 'all' | 'users' | 'list' | 'group';
@Column({
...id(),
nullable: true
})
public userListId: UserList['id'] | null;
@ManyToOne(type => UserList, {
onDelete: 'CASCADE'
})
@JoinColumn()
public userList: UserList | null;
@Column({
...id(),
nullable: true
})
public userGroupJoiningId: UserGroupJoining['id'] | null;
@ManyToOne(type => UserGroupJoining, {
onDelete: 'CASCADE'
})
@JoinColumn()
public userGroupJoining: UserGroupJoining | null;
@Column('varchar', {
length: 1024, array: true,
default: '{}'
})
public users: string[];
@Column('jsonb', {
default: []
})
public keywords: string[][];
@Column('jsonb', {
default: []
})
public excludeKeywords: string[][];
@Column('boolean', {
default: false
})
public caseSensitive: boolean;
@Column('boolean', {
default: false
})
public withReplies: boolean;
@Column('boolean')
public withFile: boolean;
@Column('varchar', {
length: 2048, nullable: true,
})
public expression: string | null;
@Column('boolean')
public notify: boolean;
}

View File

@ -0,0 +1,60 @@
import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class App {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the App.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The owner ID.'
})
public userId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'SET NULL',
nullable: true,
})
public user: User | null;
@Index()
@Column('varchar', {
length: 64,
comment: 'The secret key of the App.'
})
public secret: string;
@Column('varchar', {
length: 128,
comment: 'The name of the App.'
})
public name: string;
@Column('varchar', {
length: 512,
comment: 'The description of the App.'
})
public description: string;
@Column('varchar', {
length: 64, array: true,
comment: 'The permission of the App.'
})
public permission: string[];
@Column('varchar', {
length: 512, nullable: true,
comment: 'The callbackUrl of the App.'
})
public callbackUrl: string | null;
}

View File

@ -0,0 +1,46 @@
import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class AttestationChallenge {
@PrimaryColumn(id())
public id: string;
@Index()
@PrimaryColumn(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column('varchar', {
length: 64,
comment: 'Hex-encoded sha256 hash of the challenge.'
})
public challenge: string;
@Column('timestamp with time zone', {
comment: 'The date challenge was created for expiry purposes.'
})
public createdAt: Date;
@Column('boolean', {
comment:
'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.',
default: false
})
public registrationChallenge: boolean;
constructor(data: Partial<AttestationChallenge>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,43 @@
import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user';
import { App } from './app';
import { id } from '../id';
@Entity()
export class AuthSession {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the AuthSession.'
})
public createdAt: Date;
@Index()
@Column('varchar', {
length: 128
})
public token: string;
@Column({
...id(),
nullable: true
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
nullable: true
})
@JoinColumn()
public user: User | null;
@Column(id())
public appId: App['id'];
@ManyToOne(type => App, {
onDelete: 'CASCADE'
})
@JoinColumn()
public app: App | null;
}

View File

@ -0,0 +1,42 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
@Index(['blockerId', 'blockeeId'], { unique: true })
export class Blocking {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Blocking.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The blockee user ID.'
})
public blockeeId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public blockee: User | null;
@Index()
@Column({
...id(),
comment: 'The blocker user ID.'
})
public blockerId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public blocker: User | null;
}

View File

@ -0,0 +1,43 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { Channel } from './channel';
@Entity()
@Index(['followerId', 'followeeId'], { unique: true })
export class ChannelFollowing {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the ChannelFollowing.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The followee channel ID.'
})
public followeeId: Channel['id'];
@ManyToOne(type => Channel, {
onDelete: 'CASCADE'
})
@JoinColumn()
public followee: Channel | null;
@Index()
@Column({
...id(),
comment: 'The follower user ID.'
})
public followerId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public follower: User | null;
}

View File

@ -0,0 +1,35 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { Note } from './note';
import { Channel } from './channel';
import { id } from '../id';
@Entity()
@Index(['channelId', 'noteId'], { unique: true })
export class ChannelNotePining {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the ChannelNotePining.'
})
public createdAt: Date;
@Index()
@Column(id())
public channelId: Channel['id'];
@ManyToOne(type => Channel, {
onDelete: 'CASCADE'
})
@JoinColumn()
public channel: Channel | null;
@Column(id())
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
}

View File

@ -0,0 +1,75 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { DriveFile } from './drive-file';
@Entity()
export class Channel {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Channel.'
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
nullable: true
})
public lastNotedAt: Date | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The owner ID.'
})
public userId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'SET NULL'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128,
comment: 'The name of the Channel.'
})
public name: string;
@Column('varchar', {
length: 2048, nullable: true,
comment: 'The description of the Channel.'
})
public description: string | null;
@Column({
...id(),
nullable: true,
comment: 'The ID of banner Channel.'
})
public bannerId: DriveFile['id'] | null;
@ManyToOne(type => DriveFile, {
onDelete: 'SET NULL'
})
@JoinColumn()
public banner: DriveFile | null;
@Index()
@Column('integer', {
default: 0,
comment: 'The count of notes.'
})
public notesCount: number;
@Index()
@Column('integer', {
default: 0,
comment: 'The count of users.'
})
public usersCount: number;
}

View File

@ -0,0 +1,37 @@
import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
import { Note } from './note';
import { Clip } from './clip';
import { id } from '../id';
@Entity()
@Index(['noteId', 'clipId'], { unique: true })
export class ClipNote {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
comment: 'The note ID.'
})
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
@Index()
@Column({
...id(),
comment: 'The clip ID.'
})
public clipId: Clip['id'];
@ManyToOne(type => Clip, {
onDelete: 'CASCADE'
})
@JoinColumn()
public clip: Clip | null;
}

View File

@ -0,0 +1,44 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class Clip {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the Clip.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The owner ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128,
comment: 'The name of the Clip.'
})
public name: string;
@Column('boolean', {
default: false
})
public isPublic: boolean;
@Column('varchar', {
length: 2048, nullable: true, default: null,
comment: 'The description of the Clip.'
})
public description: string | null;
}

View File

@ -0,0 +1,164 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { DriveFolder } from './drive-folder';
import { id } from '../id';
@Entity()
@Index(['userId', 'folderId', 'id'])
export class DriveFile {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the DriveFile.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The owner ID.'
})
public userId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'SET NULL'
})
@JoinColumn()
public user: User | null;
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: 'The host of owner. It will be null if the user in local.'
})
public userHost: string | null;
@Index()
@Column('varchar', {
length: 32,
comment: 'The MD5 hash of the DriveFile.'
})
public md5: string;
@Column('varchar', {
length: 256,
comment: 'The file name of the DriveFile.'
})
public name: string;
@Index()
@Column('varchar', {
length: 128,
comment: 'The content type (MIME) of the DriveFile.'
})
public type: string;
@Column('integer', {
comment: 'The file size (bytes) of the DriveFile.'
})
public size: number;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The comment of the DriveFile.'
})
public comment: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The BlurHash string.'
})
public blurhash: string | null;
@Column('jsonb', {
default: {},
comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
})
public properties: { width?: number; height?: number; avgColor?: string };
@Index()
@Column('boolean')
public storedInternal: boolean;
@Column('varchar', {
length: 512,
comment: 'The URL of the DriveFile.'
})
public url: string;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The URL of the thumbnail of the DriveFile.'
})
public thumbnailUrl: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The URL of the webpublic of the DriveFile.'
})
public webpublicUrl: string | null;
@Index({ unique: true })
@Column('varchar', {
length: 256, nullable: true,
})
public accessKey: string | null;
@Index({ unique: true })
@Column('varchar', {
length: 256, nullable: true,
})
public thumbnailAccessKey: string | null;
@Index({ unique: true })
@Column('varchar', {
length: 256, nullable: true,
})
public webpublicAccessKey: string | null;
@Index()
@Column('varchar', {
length: 512, nullable: true,
comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.'
})
public uri: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public src: string | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The parent folder ID. If null, it means the DriveFile is located in root.'
})
public folderId: DriveFolder['id'] | null;
@ManyToOne(type => DriveFolder, {
onDelete: 'SET NULL'
})
@JoinColumn()
public folder: DriveFolder | null;
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the DriveFile is NSFW.'
})
public isSensitive: boolean;
/**
* 外部の(信頼されていない)URLへの直リンクか否か
*/
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the DriveFile is direct link to remote server.'
})
public isLink: boolean;
}

View File

@ -0,0 +1,49 @@
import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class DriveFolder {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the DriveFolder.'
})
public createdAt: Date;
@Column('varchar', {
length: 128,
comment: 'The name of the DriveFolder.'
})
public name: string;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The owner ID.'
})
public userId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.'
})
public parentId: DriveFolder['id'] | null;
@ManyToOne(type => DriveFolder, {
onDelete: 'SET NULL'
})
@JoinColumn()
public parent: DriveFolder | null;
}

View File

@ -0,0 +1,51 @@
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
import { id } from '../id';
@Entity()
@Index(['name', 'host'], { unique: true })
export class Emoji {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
nullable: true
})
public updatedAt: Date | null;
@Index()
@Column('varchar', {
length: 128
})
public name: string;
@Index()
@Column('varchar', {
length: 128, nullable: true
})
public host: string | null;
@Column('varchar', {
length: 128, nullable: true
})
public category: string | null;
@Column('varchar', {
length: 512,
})
public url: string;
@Column('varchar', {
length: 512, nullable: true
})
public uri: string | null;
@Column('varchar', {
length: 64, nullable: true
})
public type: string | null;
@Column('varchar', {
array: true, length: 128, default: '{}'
})
public aliases: string[];
}

View File

@ -0,0 +1,85 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
@Index(['followerId', 'followeeId'], { unique: true })
export class FollowRequest {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the FollowRequest.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The followee user ID.'
})
public followeeId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public followee: User | null;
@Index()
@Column({
...id(),
comment: 'The follower user ID.'
})
public followerId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public follower: User | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'id of Follow Activity.'
})
public requestId: string | null;
//#region Denormalized fields
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public followerHost: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: '[Denormalized]'
})
public followerInbox: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: '[Denormalized]'
})
public followerSharedInbox: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public followeeHost: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: '[Denormalized]'
})
public followeeInbox: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: '[Denormalized]'
})
public followeeSharedInbox: string | null;
//#endregion
}

View File

@ -0,0 +1,80 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
@Index(['followerId', 'followeeId'], { unique: true })
export class Following {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Following.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The followee user ID.'
})
public followeeId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public followee: User | null;
@Index()
@Column({
...id(),
comment: 'The follower user ID.'
})
public followerId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public follower: User | null;
//#region Denormalized fields
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public followerHost: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: '[Denormalized]'
})
public followerInbox: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: '[Denormalized]'
})
public followerSharedInbox: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public followeeHost: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: '[Denormalized]'
})
public followeeInbox: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: '[Denormalized]'
})
public followeeSharedInbox: string | null;
//#endregion
}

View File

@ -0,0 +1,33 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { GalleryPost } from './gallery-post';
@Entity()
@Index(['userId', 'postId'], { unique: true })
export class GalleryLike {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column(id())
public postId: GalleryPost['id'];
@ManyToOne(type => GalleryPost, {
onDelete: 'CASCADE'
})
@JoinColumn()
public post: GalleryPost | null;
}

View File

@ -0,0 +1,79 @@
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { DriveFile } from './drive-file';
@Entity()
export class GalleryPost {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the GalleryPost.'
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
comment: 'The updated date of the GalleryPost.'
})
public updatedAt: Date;
@Column('varchar', {
length: 256,
})
public title: string;
@Column('varchar', {
length: 2048, nullable: true
})
public description: string | null;
@Index()
@Column({
...id(),
comment: 'The ID of author.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(),
array: true, default: '{}'
})
public fileIds: DriveFile['id'][];
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the post is sensitive.'
})
public isSensitive: boolean;
@Index()
@Column('integer', {
default: 0
})
public likedCount: number;
@Index()
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public tags: string[];
constructor(data: Partial<GalleryPost>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,133 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from '../../user';
import { id } from '../../../id';
@Entity()
export class ReversiGame {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the ReversiGame.'
})
public createdAt: Date;
@Column('timestamp with time zone', {
nullable: true,
comment: 'The started date of the ReversiGame.'
})
public startedAt: Date | null;
@Column(id())
public user1Id: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user1: User | null;
@Column(id())
public user2Id: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user2: User | null;
@Column('boolean', {
default: false,
})
public user1Accepted: boolean;
@Column('boolean', {
default: false,
})
public user2Accepted: boolean;
/**
* どちらのプレイヤーが先行(黒)か
* 1 ... user1
* 2 ... user2
*/
@Column('integer', {
nullable: true,
})
public black: number | null;
@Column('boolean', {
default: false,
})
public isStarted: boolean;
@Column('boolean', {
default: false,
})
public isEnded: boolean;
@Column({
...id(),
nullable: true
})
public winnerId: User['id'] | null;
@Column({
...id(),
nullable: true
})
public surrendered: User['id'] | null;
@Column('jsonb', {
default: [],
})
public logs: {
at: Date;
color: boolean;
pos: number;
}[];
@Column('varchar', {
array: true, length: 64,
})
public map: string[];
@Column('varchar', {
length: 32
})
public bw: string;
@Column('boolean', {
default: false,
})
public isLlotheo: boolean;
@Column('boolean', {
default: false,
})
public canPutEverywhere: boolean;
@Column('boolean', {
default: false,
})
public loopedBoard: boolean;
@Column('jsonb', {
nullable: true, default: null,
})
public form1: any | null;
@Column('jsonb', {
nullable: true, default: null,
})
public form2: any | null;
/**
* ログのposを文字列としてすべて連結したもののCRC32値
*/
@Column('varchar', {
length: 32, nullable: true
})
public crc32: string | null;
}

View File

@ -0,0 +1,35 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from '../../user';
import { id } from '../../../id';
@Entity()
export class ReversiMatching {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the ReversiMatching.'
})
public createdAt: Date;
@Index()
@Column(id())
public parentId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public parent: User | null;
@Index()
@Column(id())
public childId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public child: User | null;
}

View File

@ -0,0 +1,87 @@
import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class Hashtag {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column('varchar', {
length: 128
})
public name: string;
@Column({
...id(),
array: true,
})
public mentionedUserIds: User['id'][];
@Index()
@Column('integer', {
default: 0
})
public mentionedUsersCount: number;
@Column({
...id(),
array: true,
})
public mentionedLocalUserIds: User['id'][];
@Index()
@Column('integer', {
default: 0
})
public mentionedLocalUsersCount: number;
@Column({
...id(),
array: true,
})
public mentionedRemoteUserIds: User['id'][];
@Index()
@Column('integer', {
default: 0
})
public mentionedRemoteUsersCount: number;
@Column({
...id(),
array: true,
})
public attachedUserIds: User['id'][];
@Index()
@Column('integer', {
default: 0
})
public attachedUsersCount: number;
@Column({
...id(),
array: true,
})
public attachedLocalUserIds: User['id'][];
@Index()
@Column('integer', {
default: 0
})
public attachedLocalUsersCount: number;
@Column({
...id(),
array: true,
})
public attachedRemoteUserIds: User['id'][];
@Index()
@Column('integer', {
default: 0
})
public attachedRemoteUsersCount: number;
}

View File

@ -0,0 +1,180 @@
import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
import { id } from '../id';
@Entity()
export class Instance {
@PrimaryColumn(id())
public id: string;
/**
* このインスタンスを捕捉した日時
*/
@Index()
@Column('timestamp with time zone', {
comment: 'The caught date of the Instance.'
})
public caughtAt: Date;
/**
* ホスト
*/
@Index({ unique: true })
@Column('varchar', {
length: 128,
comment: 'The host of the Instance.'
})
public host: string;
/**
* インスタンスのユーザー数
*/
@Column('integer', {
default: 0,
comment: 'The count of the users of the Instance.'
})
public usersCount: number;
/**
* インスタンスの投稿数
*/
@Column('integer', {
default: 0,
comment: 'The count of the notes of the Instance.'
})
public notesCount: number;
/**
* このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
*/
@Column('integer', {
default: 0,
})
public followingCount: number;
/**
* このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
*/
@Column('integer', {
default: 0,
})
public followersCount: number;
/**
* ドライブ使用量
*/
@Column('bigint', {
default: 0,
})
public driveUsage: number;
/**
* ドライブのファイル数
*/
@Column('integer', {
default: 0,
})
public driveFiles: number;
/**
* 直近のリクエスト送信日時
*/
@Column('timestamp with time zone', {
nullable: true,
})
public latestRequestSentAt: Date | null;
/**
* 直近のリクエスト送信時のHTTPステータスコード
*/
@Column('integer', {
nullable: true,
})
public latestStatus: number | null;
/**
* 直近のリクエスト受信日時
*/
@Column('timestamp with time zone', {
nullable: true,
})
public latestRequestReceivedAt: Date | null;
/**
* このインスタンスと最後にやり取りした日時
*/
@Column('timestamp with time zone')
public lastCommunicatedAt: Date;
/**
* このインスタンスと不通かどうか
*/
@Column('boolean', {
default: false
})
public isNotResponding: boolean;
/**
* このインスタンスへの配信を停止するか
*/
@Index()
@Column('boolean', {
default: false
})
public isSuspended: boolean;
@Column('varchar', {
length: 64, nullable: true, default: null,
comment: 'The software of the Instance.'
})
public softwareName: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public softwareVersion: string | null;
@Column('boolean', {
nullable: true, default: null,
})
public openRegistrations: boolean | null;
@Column('varchar', {
length: 256, nullable: true, default: null,
})
public name: string | null;
@Column('varchar', {
length: 4096, nullable: true, default: null,
})
public description: string | null;
@Column('varchar', {
length: 128, nullable: true, default: null,
})
public maintainerName: string | null;
@Column('varchar', {
length: 256, nullable: true, default: null,
})
public maintainerEmail: string | null;
@Column('varchar', {
length: 256, nullable: true, default: null,
})
public iconUrl: string | null;
@Column('varchar', {
length: 256, nullable: true, default: null,
})
public faviconUrl: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public themeColor: string | null;
@Column('timestamp with time zone', {
nullable: true,
})
public infoUpdatedAt: Date | null;
}

View File

@ -0,0 +1,89 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { DriveFile } from './drive-file';
import { id } from '../id';
import { UserGroup } from './user-group';
@Entity()
export class MessagingMessage {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the MessagingMessage.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The sender user ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(), nullable: true,
comment: 'The recipient user ID.'
})
public recipientId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public recipient: User | null;
@Index()
@Column({
...id(), nullable: true,
comment: 'The recipient group ID.'
})
public groupId: UserGroup['id'] | null;
@ManyToOne(type => UserGroup, {
onDelete: 'CASCADE'
})
@JoinColumn()
public group: UserGroup | null;
@Column('varchar', {
length: 4096, nullable: true
})
public text: string | null;
@Column('boolean', {
default: false,
})
public isRead: boolean;
@Column('varchar', {
length: 512, nullable: true,
})
public uri: string | null;
@Column({
...id(),
array: true, default: '{}'
})
public reads: User['id'][];
@Column({
...id(),
nullable: true,
})
public fileId: DriveFile['id'] | null;
@ManyToOne(type => DriveFile, {
onDelete: 'CASCADE'
})
@JoinColumn()
public file: DriveFile | null;
}

View File

@ -0,0 +1,423 @@
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { Clip } from './clip';
@Entity()
export class Meta {
@PrimaryColumn({
type: 'varchar',
length: 32
})
public id: string;
@Column('varchar', {
length: 128, nullable: true
})
public name: string | null;
@Column('varchar', {
length: 1024, nullable: true
})
public description: string | null;
/**
* メンテナの名前
*/
@Column('varchar', {
length: 128, nullable: true
})
public maintainerName: string | null;
/**
* メンテナの連絡先
*/
@Column('varchar', {
length: 128, nullable: true
})
public maintainerEmail: string | null;
@Column('boolean', {
default: false,
})
public disableRegistration: boolean;
@Column('boolean', {
default: false,
})
public disableLocalTimeline: boolean;
@Column('boolean', {
default: false,
})
public disableGlobalTimeline: boolean;
@Column('boolean', {
default: false,
})
public useStarForReactionFallback: boolean;
@Column('varchar', {
length: 64, array: true, default: '{}'
})
public langs: string[];
@Column('varchar', {
length: 256, array: true, default: '{}'
})
public pinnedUsers: string[];
@Column('varchar', {
length: 256, array: true, default: '{}'
})
public hiddenTags: string[];
@Column('varchar', {
length: 256, array: true, default: '{}'
})
public blockedHosts: string[];
@Column('varchar', {
length: 512, array: true, default: '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}'
})
public pinnedPages: string[];
@Column({
...id(),
nullable: true,
})
public pinnedClipId: Clip['id'] | null;
@Column('varchar', {
length: 512,
nullable: true,
default: '/assets/ai.png'
})
public mascotImageUrl: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public bannerUrl: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public backgroundImageUrl: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public logoImageUrl: string | null;
@Column('varchar', {
length: 512,
nullable: true,
default: 'https://xn--931a.moe/aiart/yubitun.png'
})
public errorImageUrl: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public iconUrl: string | null;
@Column('boolean', {
default: true,
})
public cacheRemoteFiles: boolean;
@Column('boolean', {
default: false,
})
public proxyRemoteFiles: boolean;
@Column({
...id(),
nullable: true,
})
public proxyAccountId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'SET NULL'
})
@JoinColumn()
public proxyAccount: User | null;
@Column('boolean', {
default: false,
})
public emailRequiredForSignup: boolean;
@Column('boolean', {
default: false,
})
public enableHcaptcha: boolean;
@Column('varchar', {
length: 64,
nullable: true
})
public hcaptchaSiteKey: string | null;
@Column('varchar', {
length: 64,
nullable: true
})
public hcaptchaSecretKey: string | null;
@Column('boolean', {
default: false,
})
public enableRecaptcha: boolean;
@Column('varchar', {
length: 64,
nullable: true
})
public recaptchaSiteKey: string | null;
@Column('varchar', {
length: 64,
nullable: true
})
public recaptchaSecretKey: string | null;
@Column('integer', {
default: 1024,
comment: 'Drive capacity of a local user (MB)'
})
public localDriveCapacityMb: number;
@Column('integer', {
default: 32,
comment: 'Drive capacity of a remote user (MB)'
})
public remoteDriveCapacityMb: number;
@Column('integer', {
default: 500,
comment: 'Max allowed note text length in characters'
})
public maxNoteTextLength: number;
@Column('varchar', {
length: 128,
nullable: true
})
public summalyProxy: string | null;
@Column('boolean', {
default: false,
})
public enableEmail: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public email: string | null;
@Column('boolean', {
default: false,
})
public smtpSecure: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public smtpHost: string | null;
@Column('integer', {
nullable: true
})
public smtpPort: number | null;
@Column('varchar', {
length: 128,
nullable: true
})
public smtpUser: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public smtpPass: string | null;
@Column('boolean', {
default: false,
})
public enableServiceWorker: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public swPublicKey: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public swPrivateKey: string | null;
@Column('boolean', {
default: false,
})
public enableTwitterIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public twitterConsumerKey: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public twitterConsumerSecret: string | null;
@Column('boolean', {
default: false,
})
public enableGithubIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public githubClientId: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public githubClientSecret: string | null;
@Column('boolean', {
default: false,
})
public enableDiscordIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true
})
public discordClientId: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public discordClientSecret: string | null;
@Column('varchar', {
length: 128,
nullable: true
})
public deeplAuthKey: string | null;
@Column('boolean', {
default: false,
})
public deeplIsPro: boolean;
@Column('varchar', {
length: 512,
nullable: true
})
public ToSUrl: string | null;
@Column('varchar', {
length: 512,
default: 'https://github.com/misskey-dev/misskey',
nullable: false
})
public repositoryUrl: string;
@Column('varchar', {
length: 512,
default: 'https://github.com/misskey-dev/misskey/issues/new',
nullable: true
})
public feedbackUrl: string | null;
@Column('boolean', {
default: false,
})
public useObjectStorage: boolean;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageBucket: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStoragePrefix: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageBaseUrl: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageEndpoint: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageRegion: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageAccessKey: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageSecretKey: string | null;
@Column('integer', {
nullable: true
})
public objectStoragePort: number | null;
@Column('boolean', {
default: true,
})
public objectStorageUseSSL: boolean;
@Column('boolean', {
default: true,
})
public objectStorageUseProxy: boolean;
@Column('boolean', {
default: false,
})
public objectStorageSetPublicRead: boolean;
@Column('boolean', {
default: true,
})
public objectStorageS3ForcePathStyle: boolean;
}

View File

@ -0,0 +1,32 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class ModerationLog {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the ModerationLog.'
})
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128,
})
public type: string;
@Column('jsonb')
public info: Record<string, any>;
}

View File

@ -0,0 +1,48 @@
import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
import { Note } from './note';
import { User } from './user';
import { id } from '../id';
import { mutedNoteReasons } from '../../types';
@Entity()
@Index(['noteId', 'userId'], { unique: true })
export class MutedNote {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
comment: 'The note ID.'
})
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
@Index()
@Column({
...id(),
comment: 'The user ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
/**
* ミュートされた理由。
*/
@Index()
@Column('enum', {
enum: mutedNoteReasons,
comment: 'The reason of the MutedNote.'
})
public reason: typeof mutedNoteReasons[number];
}

View File

@ -0,0 +1,42 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
@Index(['muterId', 'muteeId'], { unique: true })
export class Muting {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Muting.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The mutee user ID.'
})
public muteeId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public mutee: User | null;
@Index()
@Column({
...id(),
comment: 'The muter user ID.'
})
public muterId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public muter: User | null;
}

View File

@ -0,0 +1,35 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { Note } from './note';
import { User } from './user';
import { id } from '../id';
@Entity()
@Index(['userId', 'noteId'], { unique: true })
export class NoteFavorite {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the NoteFavorite.'
})
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column(id())
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
}

View File

@ -0,0 +1,44 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { Note } from './note';
import { id } from '../id';
@Entity()
@Index(['userId', 'noteId'], { unique: true })
export class NoteReaction {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the NoteReaction.'
})
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user?: User | null;
@Index()
@Column(id())
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note?: Note | null;
// TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため)
@Column('varchar', {
length: 260
})
public reaction: string;
}

View File

@ -0,0 +1,33 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { Note } from './note';
import { id } from '../id';
@Entity()
@Index(['userId', 'threadId'], { unique: true })
export class NoteThreadMuting {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
})
public createdAt: Date;
@Index()
@Column({
...id(),
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column('varchar', {
length: 256,
})
public threadId: string;
}

View File

@ -0,0 +1,63 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { Note } from './note';
import { id } from '../id';
import { Channel } from './channel';
@Entity()
@Index(['userId', 'noteId'], { unique: true })
export class NoteUnread {
@PrimaryColumn(id())
public id: string;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column(id())
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
/**
* メンションか否か
*/
@Index()
@Column('boolean')
public isMentioned: boolean;
/**
* ダイレクト投稿か否か
*/
@Index()
@Column('boolean')
public isSpecified: boolean;
//#region Denormalized fields
@Index()
@Column({
...id(),
comment: '[Denormalized]'
})
public noteUserId: User['id'];
@Index()
@Column({
...id(),
nullable: true,
comment: '[Denormalized]'
})
public noteChannelId: Channel['id'] | null;
//#endregion
}

View File

@ -0,0 +1,52 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { Note } from './note';
import { id } from '../id';
@Entity()
@Index(['userId', 'noteId'], { unique: true })
export class NoteWatching {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the NoteWatching.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The watcher ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(),
comment: 'The target Note ID.'
})
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
//#region Denormalized fields
@Index()
@Column({
...id(),
comment: '[Denormalized]'
})
public noteUserId: Note['userId'];
//#endregion
}

View File

@ -0,0 +1,247 @@
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { User } from './user';
import { DriveFile } from './drive-file';
import { id } from '../id';
import { noteVisibilities } from '../../types';
import { Channel } from './channel';
@Entity()
@Index('IDX_NOTE_TAGS', { synchronize: false })
@Index('IDX_NOTE_MENTIONS', { synchronize: false })
@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false })
export class Note {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Note.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of reply target.'
})
public replyId: Note['id'] | null;
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public reply: Note | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of renote target.'
})
public renoteId: Note['id'] | null;
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public renote: Note | null;
@Index()
@Column('varchar', {
length: 256, nullable: true
})
public threadId: string | null;
@Column('varchar', {
length: 8192, nullable: true
})
public text: string | null;
@Column('varchar', {
length: 256, nullable: true
})
public name: string | null;
@Column('varchar', {
length: 512, nullable: true
})
public cw: string | null;
@Index()
@Column({
...id(),
comment: 'The ID of author.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('boolean', {
default: false
})
public viaMobile: boolean;
@Column('boolean', {
default: false
})
public localOnly: boolean;
@Column('smallint', {
default: 0
})
public renoteCount: number;
@Column('smallint', {
default: 0
})
public repliesCount: number;
@Column('jsonb', {
default: {}
})
public reactions: Record<string, number>;
/**
* public ... 公開
* home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
* followers ... フォロワーのみ
* specified ... visibleUserIds で指定したユーザーのみ
*/
@Column('enum', { enum: noteVisibilities })
public visibility: typeof noteVisibilities[number];
@Index({ unique: true })
@Column('varchar', {
length: 512, nullable: true,
comment: 'The URI of a note. it will be null when the note is local.'
})
public uri: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The human readable url of a note. it will be null when the note is local.'
})
public url: string | null;
@Column('integer', {
default: 0, select: false
})
public score: number;
@Index()
@Column({
...id(),
array: true, default: '{}'
})
public fileIds: DriveFile['id'][];
@Index()
@Column('varchar', {
length: 256, array: true, default: '{}'
})
public attachedFileTypes: string[];
@Index()
@Column({
...id(),
array: true, default: '{}'
})
public visibleUserIds: User['id'][];
@Index()
@Column({
...id(),
array: true, default: '{}'
})
public mentions: User['id'][];
@Column('text', {
default: '[]'
})
public mentionedRemoteUsers: string;
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public emojis: string[];
@Index()
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public tags: string[];
@Column('boolean', {
default: false
})
public hasPoll: boolean;
@Index()
@Column({
...id(),
nullable: true, default: null,
comment: 'The ID of source channel.'
})
public channelId: Channel['id'] | null;
@ManyToOne(type => Channel, {
onDelete: 'CASCADE'
})
@JoinColumn()
public channel: Channel | null;
//#region Denormalized fields
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userHost: string | null;
@Column({
...id(),
nullable: true,
comment: '[Denormalized]'
})
public replyUserId: User['id'] | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public replyUserHost: string | null;
@Column({
...id(),
nullable: true,
comment: '[Denormalized]'
})
public renoteUserId: User['id'] | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public renoteUserHost: string | null;
//#endregion
constructor(data: Partial<Note>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}
export type IMentionedRemoteUsers = {
uri: string;
url?: string;
username: string;
host: string;
}[];

View File

@ -0,0 +1,172 @@
import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { Note } from './note';
import { FollowRequest } from './follow-request';
import { UserGroupInvitation } from './user-group-invitation';
import { AccessToken } from './access-token';
import { notificationTypes } from '@/types';
@Entity()
export class Notification {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Notification.'
})
public createdAt: Date;
/**
* 通知の受信者
*/
@Index()
@Column({
...id(),
comment: 'The ID of recipient user of the Notification.'
})
public notifieeId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public notifiee: User | null;
/**
* 通知の送信者(initiator)
*/
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of sender user of the Notification.'
})
public notifierId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public notifier: User | null;
/**
* 通知の種類。
* follow - フォローされた
* mention - 投稿で自分が言及された
* reply - (自分または自分がWatchしている)投稿が返信された
* renote - (自分または自分がWatchしている)投稿がRenoteされた
* quote - (自分または自分がWatchしている)投稿が引用Renoteされた
* reaction - (自分または自分がWatchしている)投稿にリアクションされた
* pollVote - (自分または自分がWatchしている)投稿の投票に投票された
* receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
* groupInvited - グループに招待された
* app - アプリ通知
*/
@Index()
@Column('enum', {
enum: notificationTypes,
comment: 'The type of the Notification.'
})
public type: typeof notificationTypes[number];
/**
* 通知が読まれたかどうか
*/
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the Notification is read.'
})
public isRead: boolean;
@Column({
...id(),
nullable: true
})
public noteId: Note['id'] | null;
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
@Column({
...id(),
nullable: true
})
public followRequestId: FollowRequest['id'] | null;
@ManyToOne(type => FollowRequest, {
onDelete: 'CASCADE'
})
@JoinColumn()
public followRequest: FollowRequest | null;
@Column({
...id(),
nullable: true
})
public userGroupInvitationId: UserGroupInvitation['id'] | null;
@ManyToOne(type => UserGroupInvitation, {
onDelete: 'CASCADE'
})
@JoinColumn()
public userGroupInvitation: UserGroupInvitation | null;
@Column('varchar', {
length: 128, nullable: true
})
public reaction: string | null;
@Column('integer', {
nullable: true
})
public choice: number | null;
/**
* アプリ通知のbody
*/
@Column('varchar', {
length: 2048, nullable: true
})
public customBody: string | null;
/**
* アプリ通知のheader
* (省略時はアプリ名で表示されることを期待)
*/
@Column('varchar', {
length: 256, nullable: true
})
public customHeader: string | null;
/**
* アプリ通知のicon(URL)
* (省略時はアプリアイコンで表示されることを期待)
*/
@Column('varchar', {
length: 1024, nullable: true
})
public customIcon: string | null;
/**
* アプリ通知のアプリ(のトークン)
*/
@Index()
@Column({
...id(),
nullable: true
})
public appAccessTokenId: AccessToken['id'] | null;
@ManyToOne(type => AccessToken, {
onDelete: 'CASCADE'
})
@JoinColumn()
public appAccessToken: AccessToken | null;
}

View File

@ -0,0 +1,33 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { Page } from './page';
@Entity()
@Index(['userId', 'pageId'], { unique: true })
export class PageLike {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column(id())
public pageId: Page['id'];
@ManyToOne(type => Page, {
onDelete: 'CASCADE'
})
@JoinColumn()
public page: Page | null;
}

View File

@ -0,0 +1,121 @@
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { DriveFile } from './drive-file';
@Entity()
@Index(['userId', 'name'], { unique: true })
export class Page {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Page.'
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
comment: 'The updated date of the Page.'
})
public updatedAt: Date;
@Column('varchar', {
length: 256,
})
public title: string;
@Index()
@Column('varchar', {
length: 256,
})
public name: string;
@Column('varchar', {
length: 256, nullable: true
})
public summary: string | null;
@Column('boolean')
public alignCenter: boolean;
@Column('boolean', {
default: false
})
public hideTitleWhenPinned: boolean;
@Column('varchar', {
length: 32,
})
public font: string;
@Index()
@Column({
...id(),
comment: 'The ID of author.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column({
...id(),
nullable: true,
})
public eyeCatchingImageId: DriveFile['id'] | null;
@ManyToOne(type => DriveFile, {
onDelete: 'CASCADE'
})
@JoinColumn()
public eyeCatchingImage: DriveFile | null;
@Column('jsonb', {
default: []
})
public content: Record<string, any>[];
@Column('jsonb', {
default: []
})
public variables: Record<string, any>[];
@Column('varchar', {
length: 16384,
default: ''
})
public script: string;
/**
* public ... 公開
* followers ... フォロワーのみ
* specified ... visibleUserIds で指定したユーザーのみ
*/
@Column('enum', { enum: ['public', 'followers', 'specified'] })
public visibility: 'public' | 'followers' | 'specified';
@Index()
@Column({
...id(),
array: true, default: '{}'
})
public visibleUserIds: User['id'][];
@Column('integer', {
default: 0
})
public likedCount: number;
constructor(data: Partial<Page>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,30 @@
import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
import { id } from '../id';
import { User } from './user';
@Entity()
export class PasswordResetRequest {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index({ unique: true })
@Column('varchar', {
length: 256,
})
public token: string;
@Index()
@Column({
...id(),
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
}

View File

@ -0,0 +1,40 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { Note } from './note';
import { id } from '../id';
@Entity()
@Index(['userId', 'noteId', 'choice'], { unique: true })
export class PollVote {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the PollVote.'
})
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column(id())
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
@Column('integer')
public choice: number;
}

View File

@ -0,0 +1,72 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
import { id } from '../id';
import { Note } from './note';
import { User } from './user';
import { noteVisibilities } from '../../types';
@Entity()
export class Poll {
@PrimaryColumn(id())
public noteId: Note['id'];
@OneToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
@Column('timestamp with time zone', {
nullable: true
})
public expiresAt: Date | null;
@Column('boolean')
public multiple: boolean;
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public choices: string[];
@Column('integer', {
array: true,
})
public votes: number[];
//#region Denormalized fields
@Column('enum', {
enum: noteVisibilities,
comment: '[Denormalized]'
})
public noteVisibility: typeof noteVisibilities[number];
@Index()
@Column({
...id(),
comment: '[Denormalized]'
})
public userId: User['id'];
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userHost: string | null;
//#endregion
constructor(data: Partial<Poll>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}
export type IPoll = {
choices: string[];
votes?: number[];
multiple: boolean;
expiresAt: Date | null;
};

View File

@ -0,0 +1,28 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
import { Note } from './note';
import { User } from './user';
import { id } from '../id';
@Entity()
export class PromoNote {
@PrimaryColumn(id())
public noteId: Note['id'];
@OneToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
@Column('timestamp with time zone')
public expiresAt: Date;
//#region Denormalized fields
@Index()
@Column({
...id(),
comment: '[Denormalized]'
})
public userId: User['id'];
//#endregion
}

View File

@ -0,0 +1,35 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { Note } from './note';
import { User } from './user';
import { id } from '../id';
@Entity()
@Index(['userId', 'noteId'], { unique: true })
export class PromoRead {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the PromoRead.'
})
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column(id())
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
}

View File

@ -0,0 +1,17 @@
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
import { id } from '../id';
@Entity()
export class RegistrationTicket {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index({ unique: true })
@Column('varchar', {
length: 64,
})
public code: string;
}

View File

@ -0,0 +1,58 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい
@Entity()
export class RegistryItem {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the RegistryItem.'
})
public createdAt: Date;
@Column('timestamp with time zone', {
comment: 'The updated date of the RegistryItem.'
})
public updatedAt: Date;
@Index()
@Column({
...id(),
comment: 'The owner ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 1024,
comment: 'The key of the RegistryItem.'
})
public key: string;
@Column('jsonb', {
default: {}, nullable: true,
comment: 'The value of the RegistryItem.'
})
public value: any | null;
@Index()
@Column('varchar', {
length: 1024, array: true, default: '{}'
})
public scope: string[];
// サードパーティアプリに開放するときのためのカラム
@Index()
@Column('varchar', {
length: 512, nullable: true
})
public domain: string | null;
}

View File

@ -0,0 +1,19 @@
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
import { id } from '../id';
@Entity()
export class Relay {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column('varchar', {
length: 512, nullable: false,
})
public inbox: string;
@Column('enum', {
enum: ['requesting', 'accepted', 'rejected'],
})
public status: 'requesting' | 'accepted' | 'rejected';
}

View File

@ -0,0 +1,35 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class Signin {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the Signin.'
})
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128,
})
public ip: string;
@Column('jsonb')
public headers: Record<string, any>;
@Column('boolean')
public success: boolean;
}

View File

@ -0,0 +1,37 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class SwSubscription {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 512,
})
public endpoint: string;
@Column('varchar', {
length: 256,
})
public auth: string;
@Column('varchar', {
length: 128,
})
public publickey: string;
}

View File

@ -0,0 +1,20 @@
import { PrimaryColumn, Entity, Column } from 'typeorm';
@Entity()
export class UsedUsername {
@PrimaryColumn('varchar', {
length: 128,
})
public username: string;
@Column('timestamp with time zone')
public createdAt: Date;
constructor(data: Partial<UsedUsername>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,42 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { UserGroup } from './user-group';
import { id } from '../id';
@Entity()
@Index(['userId', 'userGroupId'], { unique: true })
export class UserGroupInvitation {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroupInvitation.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The user ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(),
comment: 'The group ID.'
})
public userGroupId: UserGroup['id'];
@ManyToOne(type => UserGroup, {
onDelete: 'CASCADE'
})
@JoinColumn()
public userGroup: UserGroup | null;
}

View File

@ -0,0 +1,42 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { UserGroup } from './user-group';
import { id } from '../id';
@Entity()
@Index(['userId', 'userGroupId'], { unique: true })
export class UserGroupJoining {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroupJoining.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The user ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(),
comment: 'The group ID.'
})
public userGroupId: UserGroup['id'];
@ManyToOne(type => UserGroup, {
onDelete: 'CASCADE'
})
@JoinColumn()
public userGroup: UserGroup | null;
}

View File

@ -0,0 +1,46 @@
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserGroup {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroup.'
})
public createdAt: Date;
@Column('varchar', {
length: 256,
})
public name: string;
@Index()
@Column({
...id(),
comment: 'The ID of owner.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('boolean', {
default: false,
})
public isPrivate: boolean;
constructor(data: Partial<UserGroup>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,33 @@
import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserKeypair {
@PrimaryColumn(id())
public userId: User['id'];
@OneToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 4096,
})
public publicKey: string;
@Column('varchar', {
length: 4096,
})
public privateKey: string;
constructor(data: Partial<UserKeypair>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,42 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { UserList } from './user-list';
import { id } from '../id';
@Entity()
@Index(['userId', 'userListId'], { unique: true })
export class UserListJoining {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the UserListJoining.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The user ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(),
comment: 'The list ID.'
})
public userListId: UserList['id'];
@ManyToOne(type => UserList, {
onDelete: 'CASCADE'
})
@JoinColumn()
public userList: UserList | null;
}

View File

@ -0,0 +1,33 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserList {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the UserList.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The owner ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128,
comment: 'The name of the UserList.'
})
public name: string;
}

View File

@ -0,0 +1,35 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { Note } from './note';
import { User } from './user';
import { id } from '../id';
@Entity()
@Index(['userId', 'noteId'], { unique: true })
export class UserNotePining {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the UserNotePinings.'
})
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column(id())
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE'
})
@JoinColumn()
public note: Note | null;
}

View File

@ -0,0 +1,32 @@
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
import { id } from '../id';
@Entity()
export class UserPending {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index({ unique: true })
@Column('varchar', {
length: 128,
})
public code: string;
@Column('varchar', {
length: 128,
})
public username: string;
@Column('varchar', {
length: 128,
})
public email: string;
@Column('varchar', {
length: 128,
})
public password: string;
}

View File

@ -0,0 +1,215 @@
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { id } from '../id';
import { User } from './user';
import { Page } from './page';
import { ffVisibility, notificationTypes } from '@/types';
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
@Entity()
export class UserProfile {
@PrimaryColumn(id())
public userId: User['id'];
@OneToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The location of the User.'
})
public location: string | null;
@Column('char', {
length: 10, nullable: true,
comment: 'The birthday (YYYY-MM-DD) of the User.'
})
public birthday: string | null;
@Column('varchar', {
length: 2048, nullable: true,
comment: 'The description (bio) of the User.'
})
public description: string | null;
@Column('jsonb', {
default: [],
})
public fields: {
name: string;
value: string;
}[];
@Column('varchar', {
length: 32, nullable: true,
})
public lang: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'Remote URL of the user.'
})
public url: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The email address of the User.'
})
public email: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public emailVerifyCode: string | null;
@Column('boolean', {
default: false,
})
public emailVerified: boolean;
@Column('jsonb', {
default: ['follow', 'receiveFollowRequest', 'groupInvited']
})
public emailNotificationTypes: string[];
@Column('boolean', {
default: false,
})
public publicReactions: boolean;
@Column('enum', {
enum: ffVisibility,
default: 'public',
})
public ffVisibility: typeof ffVisibility[number];
@Column('varchar', {
length: 128, nullable: true,
})
public twoFactorTempSecret: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public twoFactorSecret: string | null;
@Column('boolean', {
default: false,
})
public twoFactorEnabled: boolean;
@Column('boolean', {
default: false,
})
public securityKeysAvailable: boolean;
@Column('boolean', {
default: false,
})
public usePasswordLessLogin: boolean;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The password hash of the User. It will be null if the origin of the user is local.'
})
public password: string | null;
// TODO: そのうち消す
@Column('jsonb', {
default: {},
comment: 'The client-specific data of the User.'
})
public clientData: Record<string, any>;
@Column('jsonb', {
default: {},
comment: 'The room data of the User.'
})
public room: Record<string, any>;
@Column('boolean', {
default: false,
})
public autoAcceptFollowed: boolean;
@Column('boolean', {
default: false,
comment: 'Whether reject index by crawler.'
})
public noCrawle: boolean;
@Column('boolean', {
default: false,
})
public alwaysMarkNsfw: boolean;
@Column('boolean', {
default: false,
})
public carefulBot: boolean;
@Column('boolean', {
default: true,
})
public injectFeaturedNote: boolean;
@Column('boolean', {
default: true,
})
public receiveAnnouncementEmail: boolean;
@Column({
...id(),
nullable: true
})
public pinnedPageId: Page['id'] | null;
@OneToOne(type => Page, {
onDelete: 'SET NULL'
})
@JoinColumn()
public pinnedPage: Page | null;
@Column('jsonb', {
default: {}
})
public integrations: Record<string, any>;
@Index()
@Column('boolean', {
default: false, select: false,
})
public enableWordMute: boolean;
@Column('jsonb', {
default: []
})
public mutedWords: string[][];
@Column('enum', {
enum: notificationTypes,
array: true,
default: [],
})
public mutingNotificationTypes: typeof notificationTypes[number][];
//#region Denormalized fields
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userHost: string | null;
//#endregion
constructor(data: Partial<UserProfile>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,34 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserPublickey {
@PrimaryColumn(id())
public userId: User['id'];
@OneToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index({ unique: true })
@Column('varchar', {
length: 256,
})
public keyId: string;
@Column('varchar', {
length: 4096,
})
public keyPem: string;
constructor(data: Partial<UserPublickey>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,48 @@
import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserSecurityKey {
@PrimaryColumn('varchar', {
comment: 'Variable-length id given to navigator.credentials.get()'
})
public id: string;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column('varchar', {
comment:
'Variable-length public key used to verify attestations (hex-encoded).'
})
public publicKey: string;
@Column('timestamp with time zone', {
comment:
'The date of the last time the UserSecurityKey was successfully validated.'
})
public lastUsed: Date;
@Column('varchar', {
comment: 'User-defined name for this key',
length: 30
})
public name: string;
constructor(data: Partial<UserSecurityKey>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,250 @@
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { DriveFile } from './drive-file';
import { id } from '../id';
@Entity()
@Index(['usernameLower', 'host'], { unique: true })
export class User {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the User.'
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
nullable: true,
comment: 'The updated date of the User.'
})
public updatedAt: Date | null;
@Column('timestamp with time zone', {
nullable: true
})
public lastFetchedAt: Date | null;
@Index()
@Column('timestamp with time zone', {
nullable: true
})
public lastActiveDate: Date | null;
@Column('boolean', {
default: false,
})
public hideOnlineStatus: boolean;
@Column('varchar', {
length: 128,
comment: 'The username of the User.'
})
public username: string;
@Index()
@Column('varchar', {
length: 128, select: false,
comment: 'The username (lowercased) of the User.'
})
public usernameLower: string;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The name of the User.'
})
public name: string | null;
@Column('integer', {
default: 0,
comment: 'The count of followers.'
})
public followersCount: number;
@Column('integer', {
default: 0,
comment: 'The count of following.'
})
public followingCount: number;
@Column('integer', {
default: 0,
comment: 'The count of notes.'
})
public notesCount: number;
@Column({
...id(),
nullable: true,
comment: 'The ID of avatar DriveFile.'
})
public avatarId: DriveFile['id'] | null;
@OneToOne(type => DriveFile, {
onDelete: 'SET NULL'
})
@JoinColumn()
public avatar: DriveFile | null;
@Column({
...id(),
nullable: true,
comment: 'The ID of banner DriveFile.'
})
public bannerId: DriveFile['id'] | null;
@OneToOne(type => DriveFile, {
onDelete: 'SET NULL'
})
@JoinColumn()
public banner: DriveFile | null;
@Index()
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public tags: string[];
@Column('varchar', {
length: 512, nullable: true,
})
public avatarUrl: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public bannerUrl: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public avatarBlurhash: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public bannerBlurhash: string | null;
@Column('boolean', {
default: false,
comment: 'Whether the User is suspended.'
})
public isSuspended: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is silenced.'
})
public isSilenced: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is locked.'
})
public isLocked: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is a bot.'
})
public isBot: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is a cat.'
})
public isCat: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is the admin.'
})
public isAdmin: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is a moderator.'
})
public isModerator: boolean;
@Index()
@Column('boolean', {
default: true,
comment: 'Whether the User is explorable.'
})
public isExplorable: boolean;
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
@Column('boolean', {
default: false,
comment: 'Whether the User is deleted.'
})
public isDeleted: boolean;
@Column('varchar', {
length: 128, array: true, default: '{}'
})
public emojis: string[];
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: 'The host of the User. It will be null if the origin of the user is local.'
})
public host: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The inbox URL of the User. It will be null if the origin of the user is local.'
})
public inbox: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'
})
public sharedInbox: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The featured URL of the User. It will be null if the origin of the user is local.'
})
public featured: string | null;
@Index()
@Column('varchar', {
length: 512, nullable: true,
comment: 'The URI of the User. It will be null if the origin of the user is local.'
})
public uri: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'
})
public followersUri: string | null;
@Index({ unique: true })
@Column('char', {
length: 16, nullable: true, unique: true,
comment: 'The native access token of the User. It will be null if the origin of the user is local.'
})
public token: string | null;
constructor(data: Partial<User>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}
export interface ILocalUser extends User {
host: null;
}
export interface IRemoteUser extends User {
host: string;
}

View File

@ -0,0 +1,4 @@
export const id = () => ({
type: 'varchar' as const,
length: 32
});

View File

@ -0,0 +1,130 @@
import { getRepository, getCustomRepository } from 'typeorm';
import { Announcement } from './entities/announcement';
import { AnnouncementRead } from './entities/announcement-read';
import { Instance } from './entities/instance';
import { Poll } from './entities/poll';
import { PollVote } from './entities/poll-vote';
import { Meta } from './entities/meta';
import { SwSubscription } from './entities/sw-subscription';
import { NoteWatching } from './entities/note-watching';
import { NoteThreadMuting } from './entities/note-thread-muting';
import { NoteUnread } from './entities/note-unread';
import { RegistrationTicket } from './entities/registration-tickets';
import { UserRepository } from './repositories/user';
import { NoteRepository } from './repositories/note';
import { DriveFileRepository } from './repositories/drive-file';
import { DriveFolderRepository } from './repositories/drive-folder';
import { AccessToken } from './entities/access-token';
import { UserNotePining } from './entities/user-note-pining';
import { SigninRepository } from './repositories/signin';
import { MessagingMessageRepository } from './repositories/messaging-message';
import { ReversiGameRepository } from './repositories/games/reversi/game';
import { UserListRepository } from './repositories/user-list';
import { UserListJoining } from './entities/user-list-joining';
import { UserGroupRepository } from './repositories/user-group';
import { UserGroupJoining } from './entities/user-group-joining';
import { UserGroupInvitationRepository } from './repositories/user-group-invitation';
import { FollowRequestRepository } from './repositories/follow-request';
import { MutingRepository } from './repositories/muting';
import { BlockingRepository } from './repositories/blocking';
import { NoteReactionRepository } from './repositories/note-reaction';
import { NotificationRepository } from './repositories/notification';
import { NoteFavoriteRepository } from './repositories/note-favorite';
import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
import { UserPublickey } from './entities/user-publickey';
import { UserKeypair } from './entities/user-keypair';
import { AppRepository } from './repositories/app';
import { FollowingRepository } from './repositories/following';
import { AbuseUserReportRepository } from './repositories/abuse-user-report';
import { AuthSessionRepository } from './repositories/auth-session';
import { UserProfile } from './entities/user-profile';
import { AttestationChallenge } from './entities/attestation-challenge';
import { UserSecurityKey } from './entities/user-security-key';
import { HashtagRepository } from './repositories/hashtag';
import { PageRepository } from './repositories/page';
import { PageLikeRepository } from './repositories/page-like';
import { GalleryPostRepository } from './repositories/gallery-post';
import { GalleryLikeRepository } from './repositories/gallery-like';
import { ModerationLogRepository } from './repositories/moderation-logs';
import { UsedUsername } from './entities/used-username';
import { ClipRepository } from './repositories/clip';
import { ClipNote } from './entities/clip-note';
import { AntennaRepository } from './repositories/antenna';
import { AntennaNote } from './entities/antenna-note';
import { PromoNote } from './entities/promo-note';
import { PromoRead } from './entities/promo-read';
import { EmojiRepository } from './repositories/emoji';
import { RelayRepository } from './repositories/relay';
import { ChannelRepository } from './repositories/channel';
import { MutedNote } from './entities/muted-note';
import { ChannelFollowing } from './entities/channel-following';
import { ChannelNotePining } from './entities/channel-note-pining';
import { RegistryItem } from './entities/registry-item';
import { Ad } from './entities/ad';
import { PasswordResetRequest } from './entities/password-reset-request';
import { UserPending } from './entities/user-pending';
export const Announcements = getRepository(Announcement);
export const AnnouncementReads = getRepository(AnnouncementRead);
export const Apps = getCustomRepository(AppRepository);
export const Notes = getCustomRepository(NoteRepository);
export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
export const NoteWatchings = getRepository(NoteWatching);
export const NoteThreadMutings = getRepository(NoteThreadMuting);
export const NoteReactions = getCustomRepository(NoteReactionRepository);
export const NoteUnreads = getRepository(NoteUnread);
export const Polls = getRepository(Poll);
export const PollVotes = getRepository(PollVote);
export const Users = getCustomRepository(UserRepository);
export const UserProfiles = getRepository(UserProfile);
export const UserKeypairs = getRepository(UserKeypair);
export const UserPendings = getRepository(UserPending);
export const AttestationChallenges = getRepository(AttestationChallenge);
export const UserSecurityKeys = getRepository(UserSecurityKey);
export const UserPublickeys = getRepository(UserPublickey);
export const UserLists = getCustomRepository(UserListRepository);
export const UserListJoinings = getRepository(UserListJoining);
export const UserGroups = getCustomRepository(UserGroupRepository);
export const UserGroupJoinings = getRepository(UserGroupJoining);
export const UserGroupInvitations = getCustomRepository(UserGroupInvitationRepository);
export const UserNotePinings = getRepository(UserNotePining);
export const UsedUsernames = getRepository(UsedUsername);
export const Followings = getCustomRepository(FollowingRepository);
export const FollowRequests = getCustomRepository(FollowRequestRepository);
export const Instances = getRepository(Instance);
export const Emojis = getCustomRepository(EmojiRepository);
export const DriveFiles = getCustomRepository(DriveFileRepository);
export const DriveFolders = getCustomRepository(DriveFolderRepository);
export const Notifications = getCustomRepository(NotificationRepository);
export const Metas = getRepository(Meta);
export const Mutings = getCustomRepository(MutingRepository);
export const Blockings = getCustomRepository(BlockingRepository);
export const SwSubscriptions = getRepository(SwSubscription);
export const Hashtags = getCustomRepository(HashtagRepository);
export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository);
export const RegistrationTickets = getRepository(RegistrationTicket);
export const AuthSessions = getCustomRepository(AuthSessionRepository);
export const AccessTokens = getRepository(AccessToken);
export const Signins = getCustomRepository(SigninRepository);
export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
export const ReversiGames = getCustomRepository(ReversiGameRepository);
export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
export const Pages = getCustomRepository(PageRepository);
export const PageLikes = getCustomRepository(PageLikeRepository);
export const GalleryPosts = getCustomRepository(GalleryPostRepository);
export const GalleryLikes = getCustomRepository(GalleryLikeRepository);
export const ModerationLogs = getCustomRepository(ModerationLogRepository);
export const Clips = getCustomRepository(ClipRepository);
export const ClipNotes = getRepository(ClipNote);
export const Antennas = getCustomRepository(AntennaRepository);
export const AntennaNotes = getRepository(AntennaNote);
export const PromoNotes = getRepository(PromoNote);
export const PromoReads = getRepository(PromoRead);
export const Relays = getCustomRepository(RelayRepository);
export const MutedNotes = getRepository(MutedNote);
export const Channels = getCustomRepository(ChannelRepository);
export const ChannelFollowings = getRepository(ChannelFollowing);
export const ChannelNotePinings = getRepository(ChannelNotePining);
export const RegistryItems = getRepository(RegistryItem);
export const Ads = getRepository(Ad);
export const PasswordResetRequests = getRepository(PasswordResetRequest);

View File

@ -0,0 +1,38 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../index';
import { AbuseUserReport } from '@/models/entities/abuse-user-report';
import { awaitAll } from '@/prelude/await-all';
@EntityRepository(AbuseUserReport)
export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
public async pack(
src: AbuseUserReport['id'] | AbuseUserReport,
) {
const report = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
id: report.id,
createdAt: report.createdAt,
comment: report.comment,
resolved: report.resolved,
reporterId: report.reporterId,
targetUserId: report.targetUserId,
assigneeId: report.assigneeId,
reporter: Users.pack(report.reporter || report.reporterId, null, {
detail: true
}),
targetUser: Users.pack(report.targetUser || report.targetUserId, null, {
detail: true
}),
assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, {
detail: true
}) : null,
});
}
public packMany(
reports: any[],
) {
return Promise.all(reports.map(x => this.pack(x)));
}
}

View File

@ -0,0 +1,124 @@
import { EntityRepository, Repository } from 'typeorm';
import { Antenna } from '@/models/entities/antenna';
import { Packed } from '@/misc/schema';
import { AntennaNotes, UserGroupJoinings } from '../index';
@EntityRepository(Antenna)
export class AntennaRepository extends Repository<Antenna> {
public async pack(
src: Antenna['id'] | Antenna,
): Promise<Packed<'Antenna'>> {
const antenna = typeof src === 'object' ? src : await this.findOneOrFail(src);
const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null;
const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOne(antenna.userGroupJoiningId) : null;
return {
id: antenna.id,
createdAt: antenna.createdAt.toISOString(),
name: antenna.name,
keywords: antenna.keywords,
excludeKeywords: antenna.excludeKeywords,
src: antenna.src,
userListId: antenna.userListId,
userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,
users: antenna.users,
caseSensitive: antenna.caseSensitive,
notify: antenna.notify,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
hasUnreadNote
};
}
}
export const packedAntennaSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time'
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const
},
keywords: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const
}
}
},
excludeKeywords: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const
}
}
},
src: {
type: 'string' as const,
optional: false as const, nullable: false as const,
enum: ['home', 'all', 'users', 'list', 'group']
},
userListId: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id'
},
userGroupId: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id'
},
users: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const
}
},
caseSensitive: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
default: false
},
notify: {
type: 'boolean' as const,
optional: false as const, nullable: false as const
},
withReplies: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
default: false
},
withFile: {
type: 'boolean' as const,
optional: false as const, nullable: false as const
},
hasUnreadNote: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
default: false
}
},
};

View File

@ -0,0 +1,75 @@
import { EntityRepository, Repository } from 'typeorm';
import { App } from '@/models/entities/app';
import { AccessTokens } from '../index';
import { Packed } from '@/misc/schema';
import { User } from '../entities/user';
@EntityRepository(App)
export class AppRepository extends Repository<App> {
public async pack(
src: App['id'] | App,
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: boolean,
includeSecret?: boolean,
includeProfileImageIds?: boolean
}
): Promise<Packed<'App'>> {
const opts = Object.assign({
detail: false,
includeSecret: false,
includeProfileImageIds: false
}, options);
const app = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
id: app.id,
name: app.name,
callbackUrl: app.callbackUrl,
permission: app.permission,
...(opts.includeSecret ? { secret: app.secret } : {}),
...(me ? {
isAuthorized: await AccessTokens.count({
appId: app.id,
userId: me,
}).then(count => count > 0)
} : {})
};
}
}
export const packedAppSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const
},
callbackUrl: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
permission: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const
}
},
secret: {
type: 'string' as const,
optional: true as const, nullable: false as const
},
isAuthorized: {
type: 'boolean' as const,
optional: true as const, nullable: false as const
}
}
};

View File

@ -0,0 +1,21 @@
import { EntityRepository, Repository } from 'typeorm';
import { Apps } from '../index';
import { AuthSession } from '@/models/entities/auth-session';
import { awaitAll } from '@/prelude/await-all';
import { User } from '@/models/entities/user';
@EntityRepository(AuthSession)
export class AuthSessionRepository extends Repository<AuthSession> {
public async pack(
src: AuthSession['id'] | AuthSession,
me?: { id: User['id'] } | null | undefined
) {
const session = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
id: session.id,
app: Apps.pack(session.appId, me),
token: session.token
});
}
}

View File

@ -0,0 +1,60 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../index';
import { Blocking } from '@/models/entities/blocking';
import { awaitAll } from '@/prelude/await-all';
import { Packed } from '@/misc/schema';
import { User } from '@/models/entities/user';
@EntityRepository(Blocking)
export class BlockingRepository extends Repository<Blocking> {
public async pack(
src: Blocking['id'] | Blocking,
me?: { id: User['id'] } | null | undefined
): Promise<Packed<'Blocking'>> {
const blocking = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
id: blocking.id,
createdAt: blocking.createdAt.toISOString(),
blockeeId: blocking.blockeeId,
blockee: Users.pack(blocking.blockeeId, me, {
detail: true
})
});
}
public packMany(
blockings: any[],
me: { id: User['id'] }
) {
return Promise.all(blockings.map(x => this.pack(x, me)));
}
}
export const packedBlockingSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
blockeeId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
blockee: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User' as const,
},
}
};

View File

@ -0,0 +1,95 @@
import { EntityRepository, Repository } from 'typeorm';
import { Channel } from '@/models/entities/channel';
import { Packed } from '@/misc/schema';
import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index';
import { User } from '@/models/entities/user';
@EntityRepository(Channel)
export class ChannelRepository extends Repository<Channel> {
public async pack(
src: Channel['id'] | Channel,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'Channel'>> {
const channel = typeof src === 'object' ? src : await this.findOneOrFail(src);
const meId = me ? me.id : null;
const banner = channel.bannerId ? await DriveFiles.findOne(channel.bannerId) : null;
const hasUnreadNote = meId ? (await NoteUnreads.findOne({ noteChannelId: channel.id, userId: meId })) != null : undefined;
const following = meId ? await ChannelFollowings.findOne({
followerId: meId,
followeeId: channel.id,
}) : null;
return {
id: channel.id,
createdAt: channel.createdAt.toISOString(),
lastNotedAt: channel.lastNotedAt ? channel.lastNotedAt.toISOString() : null,
name: channel.name,
description: channel.description,
userId: channel.userId,
bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
usersCount: channel.usersCount,
notesCount: channel.notesCount,
...(me ? {
isFollowing: following != null,
hasUnreadNote,
} : {})
};
}
}
export const packedChannelSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
lastNotedAt: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'date-time',
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
description: {
type: 'string' as const,
nullable: true as const, optional: false as const,
},
bannerUrl: {
type: 'string' as const,
format: 'url',
nullable: true as const, optional: false as const,
},
notesCount: {
type: 'number' as const,
nullable: false as const, optional: false as const,
},
usersCount: {
type: 'number' as const,
nullable: false as const, optional: false as const,
},
isFollowing: {
type: 'boolean' as const,
optional: true as const, nullable: false as const,
},
userId: {
type: 'string' as const,
nullable: true as const, optional: false as const,
format: 'id',
},
},
};

View File

@ -0,0 +1,70 @@
import { EntityRepository, Repository } from 'typeorm';
import { Clip } from '@/models/entities/clip';
import { Packed } from '@/misc/schema';
import { Users } from '../index';
import { awaitAll } from '@/prelude/await-all';
@EntityRepository(Clip)
export class ClipRepository extends Repository<Clip> {
public async pack(
src: Clip['id'] | Clip,
): Promise<Packed<'Clip'>> {
const clip = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
id: clip.id,
createdAt: clip.createdAt.toISOString(),
userId: clip.userId,
user: Users.pack(clip.user || clip.userId),
name: clip.name,
description: clip.description,
isPublic: clip.isPublic,
});
}
public packMany(
clips: Clip[],
) {
return Promise.all(clips.map(x => this.pack(x)));
}
}
export const packedClipSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
userId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
user: {
type: 'object' as const,
ref: 'User' as const,
optional: false as const, nullable: false as const,
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
description: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
isPublic: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
},
};

View File

@ -0,0 +1,249 @@
import { EntityRepository, Repository } from 'typeorm';
import { DriveFile } from '@/models/entities/drive-file';
import { Users, DriveFolders } from '../index';
import { User } from '@/models/entities/user';
import { toPuny } from '@/misc/convert-host';
import { awaitAll } from '@/prelude/await-all';
import { Packed } from '@/misc/schema';
import config from '@/config/index';
import { query, appendQuery } from '@/prelude/url';
import { Meta } from '@/models/entities/meta';
import { fetchMeta } from '@/misc/fetch-meta';
type PackOptions = {
detail?: boolean,
self?: boolean,
withUser?: boolean,
};
@EntityRepository(DriveFile)
export class DriveFileRepository extends Repository<DriveFile> {
public validateFileName(name: string): boolean {
return (
(name.trim().length > 0) &&
(name.length <= 200) &&
(name.indexOf('\\') === -1) &&
(name.indexOf('/') === -1) &&
(name.indexOf('..') === -1)
);
}
public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null {
// リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
return appendQuery(config.mediaProxy, query({
url: file.uri,
thumbnail: thumbnail ? '1' : undefined
}));
}
// リモートかつ期限切れはローカルプロキシを試みる
if (file.uri != null && file.isLink && meta && meta.proxyRemoteFiles) {
const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey;
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
return `${config.url}/files/${key}`;
}
}
const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml'].includes(file.type);
return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url);
}
public async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise<number> {
const id = typeof user === 'object' ? user.id : user;
const { sum } = await this
.createQueryBuilder('file')
.where('file.userId = :id', { id: id })
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) || 0;
}
public async calcDriveUsageOfHost(host: string): Promise<number> {
const { sum } = await this
.createQueryBuilder('file')
.where('file.userHost = :host', { host: toPuny(host) })
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) || 0;
}
public async calcDriveUsageOfLocal(): Promise<number> {
const { sum } = await this
.createQueryBuilder('file')
.where('file.userHost IS NULL')
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) || 0;
}
public async calcDriveUsageOfRemote(): Promise<number> {
const { sum } = await this
.createQueryBuilder('file')
.where('file.userHost IS NOT NULL')
.andWhere('file.isLink = FALSE')
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) || 0;
}
public async pack(src: DriveFile['id'], options?: PackOptions): Promise<Packed<'DriveFile'> | null>;
public async pack(src: DriveFile, options?: PackOptions): Promise<Packed<'DriveFile'>>;
public async pack(
src: DriveFile['id'] | DriveFile,
options?: PackOptions
): Promise<Packed<'DriveFile'> | null> {
const opts = Object.assign({
detail: false,
self: false
}, options);
const file = typeof src === 'object' ? src : await this.findOne(src);
if (file == null) return null;
const meta = await fetchMeta();
return await awaitAll({
id: file.id,
createdAt: file.createdAt.toISOString(),
name: file.name,
type: file.type,
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: file.properties,
url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
thumbnailUrl: this.getPublicUrl(file, true, meta),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
detail: true
}) : null,
userId: opts.withUser ? file.userId : null,
user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null
});
}
public async packMany(
files: (DriveFile['id'] | DriveFile)[],
options?: PackOptions
) {
const items = await Promise.all(files.map(f => this.pack(f, options)));
return items.filter(x => x != null);
}
}
export const packedDriveFileSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
example: 'lenna.jpg'
},
type: {
type: 'string' as const,
optional: false as const, nullable: false as const,
example: 'image/jpeg'
},
md5: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'md5',
example: '15eca7fba0480996e2245f5185bf39f2'
},
size: {
type: 'number' as const,
optional: false as const, nullable: false as const,
example: 51469
},
isSensitive: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
blurhash: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
properties: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
width: {
type: 'number' as const,
optional: true as const, nullable: false as const,
example: 1280
},
height: {
type: 'number' as const,
optional: true as const, nullable: false as const,
example: 720
},
avgColor: {
type: 'string' as const,
optional: true as const, nullable: false as const,
example: 'rgb(40,65,87)'
}
}
},
url: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'url',
},
thumbnailUrl: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'url',
},
comment: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
folderId: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
folder: {
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'DriveFolder' as const,
},
userId: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
user: {
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'User' as const,
}
},
};

View File

@ -0,0 +1,91 @@
import { EntityRepository, Repository } from 'typeorm';
import { DriveFolders, DriveFiles } from '../index';
import { DriveFolder } from '@/models/entities/drive-folder';
import { awaitAll } from '@/prelude/await-all';
import { Packed } from '@/misc/schema';
@EntityRepository(DriveFolder)
export class DriveFolderRepository extends Repository<DriveFolder> {
public validateFolderName(name: string): boolean {
return (
(name.trim().length > 0) &&
(name.length <= 200)
);
}
public async pack(
src: DriveFolder['id'] | DriveFolder,
options?: {
detail: boolean
}
): Promise<Packed<'DriveFolder'>> {
const opts = Object.assign({
detail: false
}, options);
const folder = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
id: folder.id,
createdAt: folder.createdAt.toISOString(),
name: folder.name,
parentId: folder.parentId,
...(opts.detail ? {
foldersCount: DriveFolders.count({
parentId: folder.id
}),
filesCount: DriveFiles.count({
folderId: folder.id
}),
...(folder.parentId ? {
parent: this.pack(folder.parentId, {
detail: true
})
} : {})
} : {})
});
}
}
export const packedDriveFolderSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
foldersCount: {
type: 'number' as const,
optional: true as const, nullable: false as const,
},
filesCount: {
type: 'number' as const,
optional: true as const, nullable: false as const,
},
parentId: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
parent: {
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'DriveFolder' as const,
},
},
};

View File

@ -0,0 +1,65 @@
import { EntityRepository, Repository } from 'typeorm';
import { Emoji } from '@/models/entities/emoji';
import { Packed } from '@/misc/schema';
@EntityRepository(Emoji)
export class EmojiRepository extends Repository<Emoji> {
public async pack(
src: Emoji['id'] | Emoji,
): Promise<Packed<'Emoji'>> {
const emoji = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
id: emoji.id,
aliases: emoji.aliases,
name: emoji.name,
category: emoji.category,
host: emoji.host,
url: emoji.url,
};
}
public packMany(
emojis: any[],
) {
return Promise.all(emojis.map(x => this.pack(x)));
}
}
export const packedEmojiSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
aliases: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
category: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
host: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
url: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
}
};

View File

@ -0,0 +1,106 @@
import config from '@/config/index';
export const packedFederationInstanceSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
},
caughtAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time'
},
host: {
type: 'string' as const,
optional: false as const, nullable: false as const,
example: 'misskey.example.com'
},
usersCount: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
notesCount: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
followingCount: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
followersCount: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
driveUsage: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
driveFiles: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
latestRequestSentAt: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'date-time'
},
lastCommunicatedAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time'
},
isNotResponding: {
type: 'boolean' as const,
optional: false as const, nullable: false as const
},
isSuspended: {
type: 'boolean' as const,
optional: false as const, nullable: false as const
},
softwareName: {
type: 'string' as const,
optional: false as const, nullable: true as const,
example: 'misskey'
},
softwareVersion: {
type: 'string' as const,
optional: false as const, nullable: true as const,
example: config.version
},
openRegistrations: {
type: 'boolean' as const,
optional: false as const, nullable: true as const,
example: true
},
name: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
description: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
maintainerName: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
maintainerEmail: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
iconUrl: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'url'
},
infoUpdatedAt: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'date-time'
}
}
};

View File

@ -0,0 +1,20 @@
import { EntityRepository, Repository } from 'typeorm';
import { FollowRequest } from '@/models/entities/follow-request';
import { Users } from '../index';
import { User } from '@/models/entities/user';
@EntityRepository(FollowRequest)
export class FollowRequestRepository extends Repository<FollowRequest> {
public async pack(
src: FollowRequest['id'] | FollowRequest,
me?: { id: User['id'] } | null | undefined
) {
const request = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
id: request.id,
follower: await Users.pack(request.followerId, me),
followee: await Users.pack(request.followeeId, me),
};
}
}

View File

@ -0,0 +1,124 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../index';
import { Following } from '@/models/entities/following';
import { awaitAll } from '@/prelude/await-all';
import { Packed } from '@/misc/schema';
import { User } from '@/models/entities/user';
type LocalFollowerFollowing = Following & {
followerHost: null;
followerInbox: null;
followerSharedInbox: null;
};
type RemoteFollowerFollowing = Following & {
followerHost: string;
followerInbox: string;
followerSharedInbox: string;
};
type LocalFolloweeFollowing = Following & {
followeeHost: null;
followeeInbox: null;
followeeSharedInbox: null;
};
type RemoteFolloweeFollowing = Following & {
followeeHost: string;
followeeInbox: string;
followeeSharedInbox: string;
};
@EntityRepository(Following)
export class FollowingRepository extends Repository<Following> {
public isLocalFollower(following: Following): following is LocalFollowerFollowing {
return following.followerHost == null;
}
public isRemoteFollower(following: Following): following is RemoteFollowerFollowing {
return following.followerHost != null;
}
public isLocalFollowee(following: Following): following is LocalFolloweeFollowing {
return following.followeeHost == null;
}
public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing {
return following.followeeHost != null;
}
public async pack(
src: Following['id'] | Following,
me?: { id: User['id'] } | null | undefined,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
}
): Promise<Packed<'Following'>> {
const following = typeof src === 'object' ? src : await this.findOneOrFail(src);
if (opts == null) opts = {};
return await awaitAll({
id: following.id,
createdAt: following.createdAt.toISOString(),
followeeId: following.followeeId,
followerId: following.followerId,
followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, {
detail: true
}) : undefined,
follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, {
detail: true
}) : undefined,
});
}
public packMany(
followings: any[],
me?: { id: User['id'] } | null | undefined,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
}
) {
return Promise.all(followings.map(x => this.pack(x, me, opts)));
}
}
export const packedFollowingSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
followeeId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
followee: {
type: 'object' as const,
optional: true as const, nullable: false as const,
ref: 'User' as const,
},
followerId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
follower: {
type: 'object' as const,
optional: true as const, nullable: false as const,
ref: 'User' as const,
},
}
};

View File

@ -0,0 +1,25 @@
import { EntityRepository, Repository } from 'typeorm';
import { GalleryLike } from '@/models/entities/gallery-like';
import { GalleryPosts } from '../index';
@EntityRepository(GalleryLike)
export class GalleryLikeRepository extends Repository<GalleryLike> {
public async pack(
src: GalleryLike['id'] | GalleryLike,
me?: any
) {
const like = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
id: like.id,
post: await GalleryPosts.pack(like.post || like.postId, me),
};
}
public packMany(
likes: any[],
me: any
) {
return Promise.all(likes.map(x => this.pack(x, me)));
}
}

View File

@ -0,0 +1,111 @@
import { EntityRepository, Repository } from 'typeorm';
import { GalleryPost } from '@/models/entities/gallery-post';
import { Packed } from '@/misc/schema';
import { Users, DriveFiles, GalleryLikes } from '../index';
import { awaitAll } from '@/prelude/await-all';
import { User } from '@/models/entities/user';
@EntityRepository(GalleryPost)
export class GalleryPostRepository extends Repository<GalleryPost> {
public async pack(
src: GalleryPost['id'] | GalleryPost,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'GalleryPost'>> {
const meId = me ? me.id : null;
const post = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
id: post.id,
createdAt: post.createdAt.toISOString(),
updatedAt: post.updatedAt.toISOString(),
userId: post.userId,
user: Users.pack(post.user || post.userId, me),
title: post.title,
description: post.description,
fileIds: post.fileIds,
files: DriveFiles.packMany(post.fileIds),
tags: post.tags.length > 0 ? post.tags : undefined,
isSensitive: post.isSensitive,
likedCount: post.likedCount,
isLiked: meId ? await GalleryLikes.findOne({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
});
}
public packMany(
posts: GalleryPost[],
me?: { id: User['id'] } | null | undefined,
) {
return Promise.all(posts.map(x => this.pack(x, me)));
}
}
export const packedGalleryPostSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
updatedAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
title: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
description: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
userId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
user: {
type: 'object' as const,
ref: 'User' as const,
optional: false as const, nullable: false as const,
},
fileIds: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
}
},
files: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFile' as const,
}
},
tags: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const,
}
},
isSensitive: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
}
};

View File

@ -0,0 +1,191 @@
import { User } from '@/models/entities/user';
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../../../index';
import { ReversiGame } from '@/models/entities/games/reversi/game';
import { Packed } from '@/misc/schema';
@EntityRepository(ReversiGame)
export class ReversiGameRepository extends Repository<ReversiGame> {
public async pack(
src: ReversiGame['id'] | ReversiGame,
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: boolean
}
): Promise<Packed<'ReversiGame'>> {
const opts = Object.assign({
detail: true
}, options);
const game = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
id: game.id,
createdAt: game.createdAt.toISOString(),
startedAt: game.startedAt && game.startedAt.toISOString(),
isStarted: game.isStarted,
isEnded: game.isEnded,
form1: game.form1,
form2: game.form2,
user1Accepted: game.user1Accepted,
user2Accepted: game.user2Accepted,
user1Id: game.user1Id,
user2Id: game.user2Id,
user1: await Users.pack(game.user1Id, me),
user2: await Users.pack(game.user2Id, me),
winnerId: game.winnerId,
winner: game.winnerId ? await Users.pack(game.winnerId, me) : null,
surrendered: game.surrendered,
black: game.black,
bw: game.bw,
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
...(opts.detail ? {
logs: game.logs.map(log => ({
at: log.at.toISOString(),
color: log.color,
pos: log.pos
})),
map: game.map,
} : {})
};
}
}
export const packedReversiGameSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
startedAt: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'date-time',
},
isStarted: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
isEnded: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
form1: {
type: 'any' as const,
optional: false as const, nullable: true as const,
},
form2: {
type: 'any' as const,
optional: false as const, nullable: true as const,
},
user1Accepted: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
user2Accepted: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
user1Id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
user2Id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
user1: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User' as const,
},
user2: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User' as const,
},
winnerId: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
winner: {
type: 'object' as const,
optional: false as const, nullable: true as const,
ref: 'User' as const,
},
surrendered: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
black: {
type: 'number' as const,
optional: false as const, nullable: true as const,
},
bw: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
isLlotheo: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
canPutEverywhere: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
loopedBoard: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
logs: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'object' as const,
optional: true as const, nullable: false as const,
properties: {
at: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
color: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
pos: {
type: 'number' as const,
optional: false as const, nullable: false as const,
},
}
}
},
map: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const,
}
}
}
};

View File

@ -0,0 +1,69 @@
import { EntityRepository, Repository } from 'typeorm';
import { ReversiMatching } from '@/models/entities/games/reversi/matching';
import { Users } from '../../../index';
import { awaitAll } from '@/prelude/await-all';
import { User } from '@/models/entities/user';
import { Packed } from '@/misc/schema';
@EntityRepository(ReversiMatching)
export class ReversiMatchingRepository extends Repository<ReversiMatching> {
public async pack(
src: ReversiMatching['id'] | ReversiMatching,
me: { id: User['id'] }
): Promise<Packed<'ReversiMatching'>> {
const matching = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
id: matching.id,
createdAt: matching.createdAt.toISOString(),
parentId: matching.parentId,
parent: Users.pack(matching.parentId, me, {
detail: true
}),
childId: matching.childId,
child: Users.pack(matching.childId, me, {
detail: true
})
});
}
}
export const packedReversiMatchingSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
parentId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
parent: {
type: 'object' as const,
optional: false as const, nullable: true as const,
ref: 'User' as const,
},
childId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
child: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User' as const,
},
}
};

View File

@ -0,0 +1,62 @@
import { EntityRepository, Repository } from 'typeorm';
import { Hashtag } from '@/models/entities/hashtag';
import { Packed } from '@/misc/schema';
@EntityRepository(Hashtag)
export class HashtagRepository extends Repository<Hashtag> {
public async pack(
src: Hashtag,
): Promise<Packed<'Hashtag'>> {
return {
tag: src.name,
mentionedUsersCount: src.mentionedUsersCount,
mentionedLocalUsersCount: src.mentionedLocalUsersCount,
mentionedRemoteUsersCount: src.mentionedRemoteUsersCount,
attachedUsersCount: src.attachedUsersCount,
attachedLocalUsersCount: src.attachedLocalUsersCount,
attachedRemoteUsersCount: src.attachedRemoteUsersCount,
};
}
public packMany(
hashtags: Hashtag[],
) {
return Promise.all(hashtags.map(x => this.pack(x)));
}
}
export const packedHashtagSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
tag: {
type: 'string' as const,
optional: false as const, nullable: false as const,
example: 'misskey',
},
mentionedUsersCount: {
type: 'number' as const,
optional: false as const, nullable: false as const,
},
mentionedLocalUsersCount: {
type: 'number' as const,
optional: false as const, nullable: false as const,
},
mentionedRemoteUsersCount: {
type: 'number' as const,
optional: false as const, nullable: false as const,
},
attachedUsersCount: {
type: 'number' as const,
optional: false as const, nullable: false as const,
},
attachedLocalUsersCount: {
type: 'number' as const,
optional: false as const, nullable: false as const,
},
attachedRemoteUsersCount: {
type: 'number' as const,
optional: false as const, nullable: false as const,
},
}
};

View File

@ -0,0 +1,119 @@
import { EntityRepository, Repository } from 'typeorm';
import { MessagingMessage } from '@/models/entities/messaging-message';
import { Users, DriveFiles, UserGroups } from '../index';
import { Packed } from '@/misc/schema';
import { User } from '@/models/entities/user';
@EntityRepository(MessagingMessage)
export class MessagingMessageRepository extends Repository<MessagingMessage> {
public validateText(text: string): boolean {
return text.trim().length <= 1000 && text.trim() != '';
}
public async pack(
src: MessagingMessage['id'] | MessagingMessage,
me?: { id: User['id'] } | null | undefined,
options?: {
populateRecipient?: boolean,
populateGroup?: boolean,
}
): Promise<Packed<'MessagingMessage'>> {
const opts = options || {
populateRecipient: true,
populateGroup: true,
};
const message = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
id: message.id,
createdAt: message.createdAt.toISOString(),
text: message.text,
userId: message.userId,
user: await Users.pack(message.user || message.userId, me),
recipientId: message.recipientId,
recipient: message.recipientId && opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined,
groupId: message.groupId,
group: message.groupId && opts.populateGroup ? await UserGroups.pack(message.group || message.groupId) : undefined,
fileId: message.fileId,
file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
isRead: message.isRead,
reads: message.reads,
};
}
}
export const packedMessagingMessageSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
userId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
user: {
type: 'object' as const,
ref: 'User' as const,
optional: true as const, nullable: false as const,
},
text: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
fileId: {
type: 'string' as const,
optional: true as const, nullable: true as const,
format: 'id',
},
file: {
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'DriveFile' as const,
},
recipientId: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
},
recipient: {
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'User' as const,
},
groupId: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
},
group: {
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'UserGroup' as const,
},
isRead: {
type: 'boolean' as const,
optional: true as const, nullable: false as const,
},
reads: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
}
},
},
};

View File

@ -0,0 +1,30 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../index';
import { ModerationLog } from '@/models/entities/moderation-log';
import { awaitAll } from '@/prelude/await-all';
@EntityRepository(ModerationLog)
export class ModerationLogRepository extends Repository<ModerationLog> {
public async pack(
src: ModerationLog['id'] | ModerationLog,
) {
const log = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
id: log.id,
createdAt: log.createdAt,
type: log.type,
info: log.info,
userId: log.userId,
user: Users.pack(log.user || log.userId, null, {
detail: true
}),
});
}
public packMany(
reports: any[],
) {
return Promise.all(reports.map(x => this.pack(x)));
}
}

View File

@ -0,0 +1,60 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../index';
import { Muting } from '@/models/entities/muting';
import { awaitAll } from '@/prelude/await-all';
import { Packed } from '@/misc/schema';
import { User } from '@/models/entities/user';
@EntityRepository(Muting)
export class MutingRepository extends Repository<Muting> {
public async pack(
src: Muting['id'] | Muting,
me?: { id: User['id'] } | null | undefined
): Promise<Packed<'Muting'>> {
const muting = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
id: muting.id,
createdAt: muting.createdAt.toISOString(),
muteeId: muting.muteeId,
mutee: Users.pack(muting.muteeId, me, {
detail: true
})
});
}
public packMany(
mutings: any[],
me: { id: User['id'] }
) {
return Promise.all(mutings.map(x => this.pack(x, me)));
}
}
export const packedMutingSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
muteeId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
mutee: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User' as const,
},
}
};

View File

@ -0,0 +1,56 @@
import { EntityRepository, Repository } from 'typeorm';
import { NoteFavorite } from '@/models/entities/note-favorite';
import { Notes } from '../index';
import { User } from '@/models/entities/user';
@EntityRepository(NoteFavorite)
export class NoteFavoriteRepository extends Repository<NoteFavorite> {
public async pack(
src: NoteFavorite['id'] | NoteFavorite,
me?: { id: User['id'] } | null | undefined
) {
const favorite = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
id: favorite.id,
createdAt: favorite.createdAt,
noteId: favorite.noteId,
note: await Notes.pack(favorite.note || favorite.noteId, me),
};
}
public packMany(
favorites: any[],
me: { id: User['id'] }
) {
return Promise.all(favorites.map(x => this.pack(x, me)));
}
}
export const packedNoteFavoriteSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
note: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'Note' as const,
},
noteId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
},
};

View File

@ -0,0 +1,60 @@
import { EntityRepository, Repository } from 'typeorm';
import { NoteReaction } from '@/models/entities/note-reaction';
import { Notes, Users } from '../index';
import { Packed } from '@/misc/schema';
import { convertLegacyReaction } from '@/misc/reaction-lib';
import { User } from '@/models/entities/user';
@EntityRepository(NoteReaction)
export class NoteReactionRepository extends Repository<NoteReaction> {
public async pack(
src: NoteReaction['id'] | NoteReaction,
me?: { id: User['id'] } | null | undefined,
options?: {
withNote: boolean;
},
): Promise<Packed<'NoteReaction'>> {
const opts = Object.assign({
withNote: false,
}, options);
const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
id: reaction.id,
createdAt: reaction.createdAt.toISOString(),
user: await Users.pack(reaction.userId, me),
type: convertLegacyReaction(reaction.reaction),
...(opts.withNote ? {
note: await Notes.pack(reaction.noteId, me),
} : {})
};
}
}
export const packedNoteReactionSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
user: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User' as const,
},
type: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
},
};

View File

@ -0,0 +1,512 @@
import { EntityRepository, Repository, In } from 'typeorm';
import * as mfm from 'mfm-js';
import { Note } from '@/models/entities/note';
import { User } from '@/models/entities/user';
import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '../index';
import { Packed } from '@/misc/schema';
import { nyaize } from '@/misc/nyaize';
import { awaitAll } from '@/prelude/await-all';
import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib';
import { NoteReaction } from '@/models/entities/note-reaction';
import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis';
@EntityRepository(Note)
export class NoteRepository extends Repository<Note> {
public validateCw(x: string) {
return x.trim().length <= 100;
}
public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise<boolean> {
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else {
// 指定されているかどうか
const specified = note.visibleUserIds.some((id: any) => meId === id);
if (specified) {
return true;
} else {
return false;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (note.visibility === 'followers') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else if (note.reply && (meId === note.reply.userId)) {
// 自分の投稿に対するリプライ
return true;
} else if (note.mentions && note.mentions.some(id => meId === id)) {
// 自分へのメンション
return true;
} else {
// フォロワーかどうか
const following = await Followings.findOne({
followeeId: note.userId,
followerId: meId
});
if (following == null) {
return false;
} else {
return true;
}
}
}
return true;
}
private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false;
// visibility が specified かつ自分が指定されていなかったら非表示
if (packedNote.visibility === 'specified') {
if (meId == null) {
hide = true;
} else if (meId === packedNote.userId) {
hide = false;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
if (specified) {
hide = false;
} else {
hide = true;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (packedNote.visibility === 'followers') {
if (meId == null) {
hide = true;
} else if (meId === packedNote.userId) {
hide = false;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
hide = false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
// 自分へのメンション
hide = false;
} else {
// フォロワーかどうか
const following = await Followings.findOne({
followeeId: packedNote.userId,
followerId: meId
});
if (following == null) {
hide = true;
} else {
hide = false;
}
}
}
if (hide) {
packedNote.visibleUserIds = undefined;
packedNote.fileIds = [];
packedNote.files = [];
packedNote.text = null;
packedNote.poll = undefined;
packedNote.cw = null;
packedNote.isHidden = true;
}
}
public async pack(
src: Note['id'] | Note,
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: boolean;
skipHide?: boolean;
_hint_?: {
myReactions: Map<Note['id'], NoteReaction | null>;
};
}
): Promise<Packed<'Note'>> {
const opts = Object.assign({
detail: true,
skipHide: false
}, options);
const meId = me ? me.id : null;
const note = typeof src === 'object' ? src : await this.findOneOrFail(src);
const host = note.userHost;
async function populatePoll() {
const poll = await Polls.findOneOrFail(note.id);
const choices = poll.choices.map(c => ({
text: c,
votes: poll.votes[poll.choices.indexOf(c)],
isVoted: false
}));
if (poll.multiple) {
const votes = await PollVotes.find({
userId: meId!,
noteId: note.id
});
const myChoices = votes.map(v => v.choice);
for (const myChoice of myChoices) {
choices[myChoice].isVoted = true;
}
} else {
const vote = await PollVotes.findOne({
userId: meId!,
noteId: note.id
});
if (vote) {
choices[vote.choice].isVoted = true;
}
}
return {
multiple: poll.multiple,
expiresAt: poll.expiresAt,
choices
};
}
async function populateMyReaction() {
if (options?._hint_?.myReactions) {
const reaction = options._hint_.myReactions.get(note.id);
if (reaction) {
return convertLegacyReaction(reaction.reaction);
} else if (reaction === null) {
return undefined;
}
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
}
const reaction = await NoteReactions.findOne({
userId: meId!,
noteId: note.id,
});
if (reaction) {
return convertLegacyReaction(reaction.reaction);
}
return undefined;
}
let text = note.text;
if (note.name && (note.url || note.uri)) {
text = `${note.name}\n${(note.text || '').trim()}\n\n${note.url || note.uri}`;
}
const channel = note.channelId
? note.channel
? note.channel
: await Channels.findOne(note.channelId)
: null;
const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
const packed = await awaitAll({
id: note.id,
createdAt: note.createdAt.toISOString(),
userId: note.userId,
user: Users.pack(note.user || note.userId, me, {
detail: false,
}),
text: text,
cw: note.cw,
visibility: note.visibility,
localOnly: note.localOnly || undefined,
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
viaMobile: note.viaMobile || undefined,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
reactions: convertLegacyReactions(note.reactions),
tags: note.tags.length > 0 ? note.tags : undefined,
emojis: populateEmojis(note.emojis.concat(reactionEmojiNames), host),
fileIds: note.fileIds,
files: DriveFiles.packMany(note.fileIds),
replyId: note.replyId,
renoteId: note.renoteId,
channelId: note.channelId || undefined,
channel: channel ? {
id: channel.id,
name: channel.name,
} : undefined,
mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri || undefined,
url: note.url || undefined,
...(opts.detail ? {
reply: note.replyId ? this.pack(note.reply || note.replyId, me, {
detail: false,
_hint_: options?._hint_
}) : undefined,
renote: note.renoteId ? this.pack(note.renote || note.renoteId, me, {
detail: true,
_hint_: options?._hint_
}) : undefined,
poll: note.hasPoll ? populatePoll() : undefined,
...(meId ? {
myReaction: populateMyReaction()
} : {})
} : {})
});
if (packed.user.isCat && packed.text) {
const tokens = packed.text ? mfm.parse(packed.text) : [];
mfm.inspect(tokens, node => {
if (node.type === 'text') {
// TODO: quoteなtextはskip
node.props.text = nyaize(node.props.text);
}
});
packed.text = mfm.toString(tokens);
}
if (!opts.skipHide) {
await this.hideNote(packed, meId);
}
return packed;
}
public async packMany(
notes: Note[],
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: boolean;
skipHide?: boolean;
}
) {
if (notes.length === 0) return [];
const meId = me ? me.id : null;
const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
if (meId) {
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
const targets = [...notes.map(n => n.id), ...renoteIds];
const myReactions = await NoteReactions.find({
userId: meId,
noteId: In(targets),
});
for (const target of targets) {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null);
}
}
await prefetchEmojis(aggregateNoteEmojis(notes));
return await Promise.all(notes.map(n => this.pack(n, me, {
...options,
_hint_: {
myReactions: myReactionsMap
}
})));
}
}
export const packedNoteSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
text: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
cw: {
type: 'string' as const,
optional: true as const, nullable: true as const,
},
userId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
user: {
type: 'object' as const,
ref: 'User' as const,
optional: false as const, nullable: false as const,
},
replyId: {
type: 'string' as const,
optional: true as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
renoteId: {
type: 'string' as const,
optional: true as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
reply: {
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'Note' as const,
},
renote: {
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'Note' as const,
},
viaMobile: {
type: 'boolean' as const,
optional: true as const, nullable: false as const,
},
isHidden: {
type: 'boolean' as const,
optional: true as const, nullable: false as const,
},
visibility: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
mentions: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
}
},
visibleUserIds: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
}
},
fileIds: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
}
},
files: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFile' as const,
}
},
tags: {
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const,
}
},
poll: {
type: 'object' as const,
optional: true as const, nullable: true as const,
},
channelId: {
type: 'string' as const,
optional: true as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
channel: {
type: 'object' as const,
optional: true as const, nullable: true as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
name: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
},
},
},
localOnly: {
type: 'boolean' as const,
optional: true as const, nullable: false as const,
},
emojis: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
url: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
},
},
},
reactions: {
type: 'object' as const,
optional: false as const, nullable: false as const,
},
renoteCount: {
type: 'number' as const,
optional: false as const, nullable: false as const,
},
repliesCount: {
type: 'number' as const,
optional: false as const, nullable: false as const,
},
uri: {
type: 'string' as const,
optional: true as const, nullable: false as const,
},
url: {
type: 'string' as const,
optional: true as const, nullable: false as const,
},
myReaction: {
type: 'object' as const,
optional: true as const, nullable: true as const,
},
},
};

View File

@ -0,0 +1,175 @@
import { EntityRepository, In, Repository } from 'typeorm';
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index';
import { Notification } from '@/models/entities/notification';
import { awaitAll } from '@/prelude/await-all';
import { Packed } from '@/misc/schema';
import { Note } from '@/models/entities/note';
import { NoteReaction } from '@/models/entities/note-reaction';
import { User } from '@/models/entities/user';
import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis';
import { notificationTypes } from '@/types';
@EntityRepository(Notification)
export class NotificationRepository extends Repository<Notification> {
public async pack(
src: Notification['id'] | Notification,
options: {
_hintForEachNotes_?: {
myReactions: Map<Note['id'], NoteReaction | null>;
};
}
): Promise<Packed<'Notification'>> {
const notification = typeof src === 'object' ? src : await this.findOneOrFail(src);
const token = notification.appAccessTokenId ? await AccessTokens.findOneOrFail(notification.appAccessTokenId) : null;
return await awaitAll({
id: notification.id,
createdAt: notification.createdAt.toISOString(),
type: notification.type,
isRead: notification.isRead,
userId: notification.notifierId,
user: notification.notifierId ? Users.pack(notification.notifier || notification.notifierId) : null,
...(notification.type === 'mention' ? {
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_
}),
} : {}),
...(notification.type === 'reply' ? {
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_
}),
} : {}),
...(notification.type === 'renote' ? {
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_
}),
} : {}),
...(notification.type === 'quote' ? {
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_
}),
} : {}),
...(notification.type === 'reaction' ? {
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_
}),
reaction: notification.reaction
} : {}),
...(notification.type === 'pollVote' ? {
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_
}),
choice: notification.choice
} : {}),
...(notification.type === 'groupInvited' ? {
invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!),
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader || token?.name,
icon: notification.customIcon || token?.iconUrl,
} : {}),
});
}
public async packMany(
notifications: Notification[],
meId: User['id']
) {
if (notifications.length === 0) return [];
const notes = notifications.filter(x => x.note != null).map(x => x.note!);
const noteIds = notes.map(n => n.id);
const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
const targets = [...noteIds, ...renoteIds];
const myReactions = await NoteReactions.find({
userId: meId,
noteId: In(targets),
});
for (const target of targets) {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null);
}
await prefetchEmojis(aggregateNoteEmojis(notes));
return await Promise.all(notifications.map(x => this.pack(x, {
_hintForEachNotes_: {
myReactions: myReactionsMap
}
})));
}
}
export const packedNotificationSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
isRead: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
type: {
type: 'string' as const,
optional: false as const, nullable: false as const,
enum: [...notificationTypes],
},
user: {
type: 'object' as const,
ref: 'User' as const,
optional: true as const, nullable: true as const,
},
userId: {
type: 'string' as const,
optional: true as const, nullable: true as const,
format: 'id',
},
note: {
type: 'object' as const,
ref: 'Note' as const,
optional: true as const, nullable: true as const,
},
reaction: {
type: 'string' as const,
optional: true as const, nullable: true as const,
},
choice: {
type: 'number' as const,
optional: true as const, nullable: true as const,
},
invitation: {
type: 'object' as const,
optional: true as const, nullable: true as const,
},
body: {
type: 'string' as const,
optional: true as const, nullable: true as const,
},
header: {
type: 'string' as const,
optional: true as const, nullable: true as const,
},
icon: {
type: 'string' as const,
optional: true as const, nullable: true as const,
},
}
};

View File

@ -0,0 +1,26 @@
import { EntityRepository, Repository } from 'typeorm';
import { PageLike } from '@/models/entities/page-like';
import { Pages } from '../index';
import { User } from '@/models/entities/user';
@EntityRepository(PageLike)
export class PageLikeRepository extends Repository<PageLike> {
public async pack(
src: PageLike['id'] | PageLike,
me?: { id: User['id'] } | null | undefined
) {
const like = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
id: like.id,
page: await Pages.pack(like.page || like.pageId, me),
};
}
public packMany(
likes: any[],
me: { id: User['id'] }
) {
return Promise.all(likes.map(x => this.pack(x, me)));
}
}

View File

@ -0,0 +1,142 @@
import { EntityRepository, Repository } from 'typeorm';
import { Page } from '@/models/entities/page';
import { Packed } from '@/misc/schema';
import { Users, DriveFiles, PageLikes } from '../index';
import { awaitAll } from '@/prelude/await-all';
import { DriveFile } from '@/models/entities/drive-file';
import { User } from '@/models/entities/user';
@EntityRepository(Page)
export class PageRepository extends Repository<Page> {
public async pack(
src: Page['id'] | Page,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'Page'>> {
const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.findOneOrFail(src);
const attachedFiles: Promise<DriveFile | undefined>[] = [];
const collectFile = (xs: any[]) => {
for (const x of xs) {
if (x.type === 'image') {
attachedFiles.push(DriveFiles.findOne({
id: x.fileId,
userId: page.userId
}));
}
if (x.children) {
collectFile(x.children);
}
}
};
collectFile(page.content);
// 後方互換性のため
let migrated = false;
const migrate = (xs: any[]) => {
for (const x of xs) {
if (x.type === 'input') {
if (x.inputType === 'text') {
x.type = 'textInput';
}
if (x.inputType === 'number') {
x.type = 'numberInput';
if (x.default) x.default = parseInt(x.default, 10);
}
migrated = true;
}
if (x.children) {
migrate(x.children);
}
}
};
migrate(page.content);
if (migrated) {
this.update(page.id, {
content: page.content
});
}
return await awaitAll({
id: page.id,
createdAt: page.createdAt.toISOString(),
updatedAt: page.updatedAt.toISOString(),
userId: page.userId,
user: Users.pack(page.user || page.userId, me), // { detail: true } すると無限ループするので注意
content: page.content,
variables: page.variables,
title: page.title,
name: page.name,
summary: page.summary,
hideTitleWhenPinned: page.hideTitleWhenPinned,
alignCenter: page.alignCenter,
font: page.font,
script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)),
likedCount: page.likedCount,
isLiked: meId ? await PageLikes.findOne({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
});
}
public packMany(
pages: Page[],
me?: { id: User['id'] } | null | undefined,
) {
return Promise.all(pages.map(x => this.pack(x, me)));
}
}
export const packedPageSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
updatedAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
title: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
summary: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
content: {
type: 'array' as const,
optional: false as const, nullable: false as const,
},
variables: {
type: 'array' as const,
optional: false as const, nullable: false as const,
},
userId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
user: {
type: 'object' as const,
ref: 'User' as const,
optional: false as const, nullable: false as const,
},
}
};

View File

@ -0,0 +1,30 @@
export const packedQueueCountSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
waiting: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
active: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
completed: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
failed: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
delayed: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
paused: {
type: 'number' as const,
optional: false as const, nullable: false as const
}
}
};

View File

@ -0,0 +1,6 @@
import { EntityRepository, Repository } from 'typeorm';
import { Relay } from '@/models/entities/relay';
@EntityRepository(Relay)
export class RelayRepository extends Repository<Relay> {
}

View File

@ -0,0 +1,11 @@
import { EntityRepository, Repository } from 'typeorm';
import { Signin } from '@/models/entities/signin';
@EntityRepository(Signin)
export class SigninRepository extends Repository<Signin> {
public async pack(
src: Signin,
) {
return src;
}
}

View File

@ -0,0 +1,23 @@
import { EntityRepository, Repository } from 'typeorm';
import { UserGroupInvitation } from '@/models/entities/user-group-invitation';
import { UserGroups } from '../index';
@EntityRepository(UserGroupInvitation)
export class UserGroupInvitationRepository extends Repository<UserGroupInvitation> {
public async pack(
src: UserGroupInvitation['id'] | UserGroupInvitation,
) {
const invitation = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
id: invitation.id,
group: await UserGroups.pack(invitation.userGroup || invitation.userGroupId),
};
}
public packMany(
invitations: any[],
) {
return Promise.all(invitations.map(x => this.pack(x)));
}
}

View File

@ -0,0 +1,61 @@
import { EntityRepository, Repository } from 'typeorm';
import { UserGroup } from '@/models/entities/user-group';
import { UserGroupJoinings } from '../index';
import { Packed } from '@/misc/schema';
@EntityRepository(UserGroup)
export class UserGroupRepository extends Repository<UserGroup> {
public async pack(
src: UserGroup['id'] | UserGroup,
): Promise<Packed<'UserGroup'>> {
const userGroup = typeof src === 'object' ? src : await this.findOneOrFail(src);
const users = await UserGroupJoinings.find({
userGroupId: userGroup.id
});
return {
id: userGroup.id,
createdAt: userGroup.createdAt.toISOString(),
name: userGroup.name,
ownerId: userGroup.userId,
userIds: users.map(x => x.userId)
};
}
}
export const packedUserGroupSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
ownerId: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
},
userIds: {
type: 'array' as const,
nullable: false as const, optional: true as const,
items: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
}
},
},
};

View File

@ -0,0 +1,55 @@
import { EntityRepository, Repository } from 'typeorm';
import { UserList } from '@/models/entities/user-list';
import { UserListJoinings } from '../index';
import { Packed } from '@/misc/schema';
@EntityRepository(UserList)
export class UserListRepository extends Repository<UserList> {
public async pack(
src: UserList['id'] | UserList,
): Promise<Packed<'UserList'>> {
const userList = typeof src === 'object' ? src : await this.findOneOrFail(src);
const users = await UserListJoinings.find({
userListId: userList.id
});
return {
id: userList.id,
createdAt: userList.createdAt.toISOString(),
name: userList.name,
userIds: users.map(x => x.userId)
};
}
}
export const packedUserListSchema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
userIds: {
type: 'array' as const,
nullable: false as const, optional: true as const,
items: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
}
},
},
};

View File

@ -0,0 +1,659 @@
import $ from 'cafy';
import { EntityRepository, Repository, In, Not } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '../index';
import config from '@/config/index';
import { Packed } from '@/misc/schema';
import { awaitAll } from '@/prelude/await-all';
import { populateEmojis } from '@/misc/populate-emojis';
import { getAntennas } from '@/misc/antenna-cache';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const';
@EntityRepository(User)
export class UserRepository extends Repository<User> {
public async getRelation(me: User['id'], target: User['id']) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
Followings.findOne({
followerId: me,
followeeId: target
}),
Followings.findOne({
followerId: target,
followeeId: me
}),
FollowRequests.findOne({
followerId: me,
followeeId: target
}),
FollowRequests.findOne({
followerId: target,
followeeId: me
}),
Blockings.findOne({
blockerId: me,
blockeeId: target
}),
Blockings.findOne({
blockerId: target,
blockeeId: me
}),
Mutings.findOne({
muterId: me,
muteeId: target
})
]);
return {
id: target,
isFollowing: following1 != null,
hasPendingFollowRequestFromYou: followReq1 != null,
hasPendingFollowRequestToYou: followReq2 != null,
isFollowed: following2 != null,
isBlocking: toBlocking != null,
isBlocked: fromBlocked != null,
isMuted: mute != null
};
}
public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
const mute = await Mutings.find({
muterId: userId
});
const joinings = await UserGroupJoinings.find({ userId: userId });
const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
.where(`message.groupId = :groupId`, { groupId: j.userGroupId })
.andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
.getOne().then(x => x != null)));
const [withUser, withGroups] = await Promise.all([
MessagingMessages.count({
where: {
recipientId: userId,
isRead: false,
...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}),
},
take: 1
}).then(count => count > 0),
groupQs
]);
return withUser || withGroups.some(x => x);
}
public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
const reads = await AnnouncementReads.find({
userId: userId
});
const count = await Announcements.count(reads.length > 0 ? {
id: Not(In(reads.map(read => read.announcementId)))
} : {});
return count > 0;
}
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
const unread = myAntennas.length > 0 ? await AntennaNotes.findOne({
antennaId: In(myAntennas.map(x => x.id)),
read: false
}) : null;
return unread != null;
}
public async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
const channels = await ChannelFollowings.find({ followerId: userId });
const unread = channels.length > 0 ? await NoteUnreads.findOne({
userId: userId,
noteChannelId: In(channels.map(x => x.followeeId)),
}) : null;
return unread != null;
}
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
const mute = await Mutings.find({
muterId: userId
});
const mutedUserIds = mute.map(m => m.muteeId);
const count = await Notifications.count({
where: {
notifieeId: userId,
...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
isRead: false
},
take: 1
});
return count > 0;
}
public async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
const count = await FollowRequests.count({
followeeId: userId
});
return count > 0;
}
public getOnlineStatus(user: User): string {
if (user.hideOnlineStatus) return 'unknown';
if (user.lastActiveDate == null) return 'unknown';
const elapsed = Date.now() - user.lastActiveDate.getTime();
return (
elapsed < USER_ONLINE_THRESHOLD ? 'online' :
elapsed < USER_ACTIVE_THRESHOLD ? 'active' :
'offline'
);
}
public getAvatarUrl(user: User): string {
if (user.avatarUrl) {
return user.avatarUrl;
} else {
return `${config.url}/random-avatar/${user.id}`;
}
}
public async pack(
src: User['id'] | User,
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: boolean,
includeSecrets?: boolean,
}
): Promise<Packed<'User'>> {
const opts = Object.assign({
detail: false,
includeSecrets: false
}, options);
const user = typeof src === 'object' ? src : await this.findOneOrFail(src);
const meId = me ? me.id : null;
const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
const pins = opts.detail ? await UserNotePinings.createQueryBuilder('pin')
.where('pin.userId = :userId', { userId: user.id })
.innerJoinAndSelect('pin.note', 'note')
.orderBy('pin.id', 'DESC')
.getMany() : [];
const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null;
const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || (meId === user.id) ? user.followingCount :
(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followingCount :
null;
const followersCount = profile == null ? null :
(profile.ffVisibility === 'public') || (meId === user.id) ? user.followersCount :
(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followersCount :
null;
const falsy = opts.detail ? false : undefined;
const packed = {
id: user.id,
name: user.name,
username: user.username,
host: user.host,
avatarUrl: this.getAvatarUrl(user),
avatarBlurhash: user.avatarBlurhash,
avatarColor: null, // 後方互換性のため
isAdmin: user.isAdmin || falsy,
isModerator: user.isModerator || falsy,
isBot: user.isBot || falsy,
isCat: user.isCat || falsy,
instance: user.host ? Instances.findOne({ host: user.host }).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
} : undefined) : undefined,
emojis: populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),
...(opts.detail ? {
url: profile!.url,
uri: user.uri,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt?.toISOString(),
bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash,
bannerColor: null, // 後方互換性のため
isLocked: user.isLocked,
isModerator: user.isModerator || falsy,
isSilenced: user.isSilenced || falsy,
isSuspended: user.isSuspended || falsy,
description: profile!.description,
location: profile!.location,
birthday: profile!.birthday,
lang: profile!.lang,
fields: profile!.fields,
followersCount: followersCount || 0,
followingCount: followingCount || 0,
notesCount: user.notesCount,
pinnedNoteIds: pins.map(pin => pin.noteId),
pinnedNotes: Notes.packMany(pins.map(pin => pin.note!), me, {
detail: true
}),
pinnedPageId: profile!.pinnedPageId,
pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, me) : null,
publicReactions: profile!.publicReactions,
ffVisibility: profile!.ffVisibility,
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
? UserSecurityKeys.count({
userId: user.id
}).then(result => result >= 1)
: false,
} : {}),
...(opts.detail && meId === user.id ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
injectFeaturedNote: profile!.injectFeaturedNote,
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
carefulBot: profile!.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed,
noCrawle: profile!.noCrawle,
isExplorable: user.isExplorable,
isDeleted: user.isDeleted,
hideOnlineStatus: user.hideOnlineStatus,
hasUnreadSpecifiedNotes: NoteUnreads.count({
where: { userId: user.id, isSpecified: true },
take: 1
}).then(count => count > 0),
hasUnreadMentions: NoteUnreads.count({
where: { userId: user.id, isMentioned: true },
take: 1
}).then(count => count > 0),
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
hasUnreadChannel: this.getHasUnreadChannel(user.id),
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
integrations: profile!.integrations,
mutedWords: profile!.mutedWords,
mutingNotificationTypes: profile!.mutingNotificationTypes,
emailNotificationTypes: profile!.emailNotificationTypes,
} : {}),
...(opts.includeSecrets ? {
email: profile!.email,
emailVerified: profile!.emailVerified,
securityKeysList: profile!.twoFactorEnabled
? UserSecurityKeys.find({
where: {
userId: user.id
},
select: ['id', 'name', 'lastUsed']
})
: []
} : {}),
...(relation ? {
isFollowing: relation.isFollowing,
isFollowed: relation.isFollowed,
hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
isBlocking: relation.isBlocking,
isBlocked: relation.isBlocked,
isMuted: relation.isMuted,
} : {})
};
return await awaitAll(packed);
}
public packMany(
users: (User['id'] | User)[],
me?: { id: User['id'] } | null | undefined,
options?: {
detail?: boolean,
includeSecrets?: boolean,
}
) {
return Promise.all(users.map(u => this.pack(u, me, options)));
}
public isLocalUser(user: User): user is ILocalUser;
public isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
public isLocalUser(user: User | { host: User['host'] }): boolean {
return user.host == null;
}
public isRemoteUser(user: User): user is IRemoteUser;
public isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
public isRemoteUser(user: User | { host: User['host'] }): boolean {
return !this.isLocalUser(user);
}
//#region Validators
public validateLocalUsername = $.str.match(/^\w{1,20}$/);
public validatePassword = $.str.min(1);
public validateName = $.str.min(1).max(50);
public validateDescription = $.str.min(1).max(500);
public validateLocation = $.str.min(1).max(50);
public validateBirthday = $.str.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/);
//#endregion
}
export const packedUserSchema = {
type: 'object' as const,
nullable: false as const, optional: false as const,
properties: {
id: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
name: {
type: 'string' as const,
nullable: true as const, optional: false as const,
example: '藍'
},
username: {
type: 'string' as const,
nullable: false as const, optional: false as const,
example: 'ai'
},
host: {
type: 'string' as const,
nullable: true as const, optional: false as const,
example: 'misskey.example.com'
},
avatarUrl: {
type: 'string' as const,
format: 'url',
nullable: true as const, optional: false as const,
},
avatarBlurhash: {
type: 'any' as const,
nullable: true as const, optional: false as const,
},
avatarColor: {
type: 'any' as const,
nullable: true as const, optional: false as const,
default: null
},
isAdmin: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
default: false
},
isModerator: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
default: false
},
isBot: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
isCat: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
emojis: {
type: 'array' as const,
nullable: false as const, optional: false as const,
items: {
type: 'object' as const,
nullable: false as const, optional: false as const,
properties: {
name: {
type: 'string' as const,
nullable: false as const, optional: false as const
},
url: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'url'
},
}
}
},
url: {
type: 'string' as const,
format: 'url',
nullable: true as const, optional: true as const,
},
createdAt: {
type: 'string' as const,
nullable: false as const, optional: true as const,
format: 'date-time',
},
updatedAt: {
type: 'string' as const,
nullable: true as const, optional: true as const,
format: 'date-time',
},
bannerUrl: {
type: 'string' as const,
format: 'url',
nullable: true as const, optional: true as const,
},
bannerBlurhash: {
type: 'any' as const,
nullable: true as const, optional: true as const,
},
bannerColor: {
type: 'any' as const,
nullable: true as const, optional: true as const,
default: null
},
isLocked: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
isSuspended: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
example: false
},
description: {
type: 'string' as const,
nullable: true as const, optional: true as const,
example: 'Hi masters, I am Ai!'
},
location: {
type: 'string' as const,
nullable: true as const, optional: true as const,
},
birthday: {
type: 'string' as const,
nullable: true as const, optional: true as const,
example: '2018-03-12'
},
fields: {
type: 'array' as const,
nullable: false as const, optional: true as const,
items: {
type: 'object' as const,
nullable: false as const, optional: false as const,
properties: {
name: {
type: 'string' as const,
nullable: false as const, optional: false as const
},
value: {
type: 'string' as const,
nullable: false as const, optional: false as const
}
},
maxLength: 4
}
},
followersCount: {
type: 'number' as const,
nullable: false as const, optional: true as const,
},
followingCount: {
type: 'number' as const,
nullable: false as const, optional: true as const,
},
notesCount: {
type: 'number' as const,
nullable: false as const, optional: true as const,
},
pinnedNoteIds: {
type: 'array' as const,
nullable: false as const, optional: true as const,
items: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
}
},
pinnedNotes: {
type: 'array' as const,
nullable: false as const, optional: true as const,
items: {
type: 'object' as const,
nullable: false as const, optional: false as const,
ref: 'Note' as const,
}
},
pinnedPageId: {
type: 'string' as const,
nullable: true as const, optional: true as const
},
pinnedPage: {
type: 'object' as const,
nullable: true as const, optional: true as const,
ref: 'Page' as const,
},
twoFactorEnabled: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
default: false
},
usePasswordLessLogin: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
default: false
},
securityKeys: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
default: false
},
avatarId: {
type: 'string' as const,
nullable: true as const, optional: true as const,
format: 'id'
},
bannerId: {
type: 'string' as const,
nullable: true as const, optional: true as const,
format: 'id'
},
autoWatch: {
type: 'boolean' as const,
nullable: false as const, optional: true as const
},
injectFeaturedNote: {
type: 'boolean' as const,
nullable: false as const, optional: true as const
},
alwaysMarkNsfw: {
type: 'boolean' as const,
nullable: false as const, optional: true as const
},
carefulBot: {
type: 'boolean' as const,
nullable: false as const, optional: true as const
},
autoAcceptFollowed: {
type: 'boolean' as const,
nullable: false as const, optional: true as const
},
hasUnreadSpecifiedNotes: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
hasUnreadMentions: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
hasUnreadAnnouncement: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
hasUnreadAntenna: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
hasUnreadChannel: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
hasUnreadMessagingMessage: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
hasUnreadNotification: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
hasPendingReceivedFollowRequest: {
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
integrations: {
type: 'object' as const,
nullable: false as const, optional: true as const
},
mutedWords: {
type: 'array' as const,
nullable: false as const, optional: true as const
},
mutingNotificationTypes: {
type: 'array' as const,
nullable: false as const, optional: true as const
},
isFollowing: {
type: 'boolean' as const,
optional: true as const, nullable: false as const
},
hasPendingFollowRequestFromYou: {
type: 'boolean' as const,
optional: true as const, nullable: false as const
},
hasPendingFollowRequestToYou: {
type: 'boolean' as const,
optional: true as const, nullable: false as const
},
isFollowed: {
type: 'boolean' as const,
optional: true as const, nullable: false as const
},
isBlocking: {
type: 'boolean' as const,
optional: true as const, nullable: false as const
},
isBlocked: {
type: 'boolean' as const,
optional: true as const, nullable: false as const
},
isMuted: {
type: 'boolean' as const,
optional: true as const, nullable: false as const
}
},
};