mirror of
https://github.com/sim1222/misskey.git
synced 2025-08-03 23:16:28 +09:00
@ -0,0 +1,42 @@
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Blockings } from '@/models/index';
|
||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
// ここでいうBlockedは被Blockedの意
|
||||
export function generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
|
||||
const blockingQuery = Blockings.createQueryBuilder('blocking')
|
||||
.select('blocking.blockerId')
|
||||
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
|
||||
|
||||
// 投稿の作者にブロックされていない かつ
|
||||
// 投稿の返信先の作者にブロックされていない かつ
|
||||
// 投稿の引用元の作者にブロックされていない
|
||||
q
|
||||
.andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`)
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.replyUserId IS NULL`)
|
||||
.orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
|
||||
}))
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.renoteUserId IS NULL`)
|
||||
.orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
q.setParameters(blockingQuery.getParameters());
|
||||
}
|
||||
|
||||
export function generateBlockQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
|
||||
const blockingQuery = Blockings.createQueryBuilder('blocking')
|
||||
.select('blocking.blockeeId')
|
||||
.where('blocking.blockerId = :blockerId', { blockerId: me.id });
|
||||
|
||||
const blockedQuery = Blockings.createQueryBuilder('blocking')
|
||||
.select('blocking.blockerId')
|
||||
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
|
||||
|
||||
q.andWhere(`user.id NOT IN (${ blockingQuery.getQuery() })`);
|
||||
q.setParameters(blockingQuery.getParameters());
|
||||
|
||||
q.andWhere(`user.id NOT IN (${ blockedQuery.getQuery() })`);
|
||||
q.setParameters(blockedQuery.getParameters());
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { User } from '@/models/entities/user';
|
||||
import { ChannelFollowings } from '@/models/index';
|
||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
export function generateChannelQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
|
||||
if (me == null) {
|
||||
q.andWhere('note.channelId IS NULL');
|
||||
} else {
|
||||
q.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
const channelFollowingQuery = ChannelFollowings.createQueryBuilder('channelFollowing')
|
||||
.select('channelFollowing.followeeId')
|
||||
.where('channelFollowing.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
// チャンネルのノートではない
|
||||
.where('note.channelId IS NULL')
|
||||
// または自分がフォローしているチャンネルのノート
|
||||
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
q.setParameters(channelFollowingQuery.getParameters());
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { User } from '@/models/entities/user';
|
||||
import { MutedNotes } from '@/models/index';
|
||||
import { SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
export function generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
|
||||
const mutedQuery = MutedNotes.createQueryBuilder('muted')
|
||||
.select('muted.noteId')
|
||||
.where('muted.userId = :userId', { userId: me.id });
|
||||
|
||||
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
||||
|
||||
q.setParameters(mutedQuery.getParameters());
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { User } from '@/models/entities/user';
|
||||
import { NoteThreadMutings } from '@/models/index';
|
||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
export function generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
|
||||
const mutedQuery = NoteThreadMutings.createQueryBuilder('threadMuted')
|
||||
.select('threadMuted.threadId')
|
||||
.where('threadMuted.userId = :userId', { userId: me.id });
|
||||
|
||||
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.threadId IS NULL`)
|
||||
.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
q.setParameters(mutedQuery.getParameters());
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Mutings } from '@/models/index';
|
||||
import { SelectQueryBuilder, Brackets } from 'typeorm';
|
||||
|
||||
export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }, exclude?: User) {
|
||||
const mutingQuery = Mutings.createQueryBuilder('muting')
|
||||
.select('muting.muteeId')
|
||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||
|
||||
if (exclude) {
|
||||
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
|
||||
}
|
||||
|
||||
// 投稿の作者をミュートしていない かつ
|
||||
// 投稿の返信先の作者をミュートしていない かつ
|
||||
// 投稿の引用元の作者をミュートしていない
|
||||
q
|
||||
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.replyUserId IS NULL`)
|
||||
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
}))
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.renoteUserId IS NULL`)
|
||||
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
||||
|
||||
export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
|
||||
const mutingQuery = Mutings.createQueryBuilder('muting')
|
||||
.select('muting.muteeId')
|
||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||
|
||||
q
|
||||
.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { secureRndstr } from '@/misc/secure-rndstr';
|
||||
|
||||
export default () => secureRndstr(16, true);
|
@ -0,0 +1,27 @@
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
|
||||
if (me == null) {
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.replyId IS NULL`) // 返信ではない
|
||||
.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
|
||||
.where(`note.replyId IS NOT NULL`)
|
||||
.andWhere('note.replyUserId = note.userId');
|
||||
}));
|
||||
}));
|
||||
} else {
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.replyId IS NULL`) // 返信ではない
|
||||
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
|
||||
.orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信
|
||||
.where(`note.replyId IS NOT NULL`)
|
||||
.andWhere('note.userId = :meId', { meId: me.id });
|
||||
}))
|
||||
.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
|
||||
.where(`note.replyId IS NOT NULL`)
|
||||
.andWhere('note.replyUserId = note.userId');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Followings } from '@/models/index';
|
||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
|
||||
if (me == null) {
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.visibility = 'public'`)
|
||||
.orWhere(`note.visibility = 'home'`);
|
||||
}));
|
||||
} else {
|
||||
const followingQuery = Followings.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
// 公開投稿である
|
||||
.where(new Brackets(qb => { qb
|
||||
.where(`note.visibility = 'public'`)
|
||||
.orWhere(`note.visibility = 'home'`);
|
||||
}))
|
||||
// または 自分自身
|
||||
.orWhere('note.userId = :userId1', { userId1: me.id })
|
||||
// または 自分宛て
|
||||
.orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`)
|
||||
.orWhere(new Brackets(qb => { qb
|
||||
// または フォロワー宛ての投稿であり、
|
||||
.where('note.visibility = \'followers\'')
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
// 自分がフォロワーである
|
||||
.where(`note.userId IN (${ followingQuery.getQuery() })`)
|
||||
// または 自分の投稿へのリプライ
|
||||
.orWhere('note.replyUserId = :userId3', { userId3: me.id });
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
|
||||
q.setParameters(followingQuery.getParameters());
|
||||
}
|
||||
}
|
56
packages/backend/src/server/api/common/getters.ts
Normal file
56
packages/backend/src/server/api/common/getters.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { IdentifiableError } from '@/misc/identifiable-error';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { Notes, Users } from '@/models/index';
|
||||
|
||||
/**
|
||||
* Get note for API processing
|
||||
*/
|
||||
export async function getNote(noteId: Note['id']) {
|
||||
const note = await Notes.findOne(noteId);
|
||||
|
||||
if (note == null) {
|
||||
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user for API processing
|
||||
*/
|
||||
export async function getUser(userId: User['id']) {
|
||||
const user = await Users.findOne(userId);
|
||||
|
||||
if (user == null) {
|
||||
throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote user for API processing
|
||||
*/
|
||||
export async function getRemoteUser(userId: User['id']) {
|
||||
const user = await getUser(userId);
|
||||
|
||||
if (!Users.isRemoteUser(user)) {
|
||||
throw new Error('user is not a remote user');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local user for API processing
|
||||
*/
|
||||
export async function getLocalUser(userId: User['id']) {
|
||||
const user = await getUser(userId);
|
||||
|
||||
if (!Users.isLocalUser(user)) {
|
||||
throw new Error('user is not a local user');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
56
packages/backend/src/server/api/common/inject-featured.ts
Normal file
56
packages/backend/src/server/api/common/inject-featured.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import rndstr from 'rndstr';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Notes, UserProfiles, NoteReactions } from '@/models/index';
|
||||
import { generateMutedUserQuery } from './generate-muted-user-query';
|
||||
import { generateBlockedUserQuery } from './generate-block-query';
|
||||
|
||||
// TODO: リアクション、Renote、返信などをしたノートは除外する
|
||||
|
||||
export async function injectFeatured(timeline: Note[], user?: User | null) {
|
||||
if (timeline.length < 5) return;
|
||||
|
||||
if (user) {
|
||||
const profile = await UserProfiles.findOneOrFail(user.id);
|
||||
if (!profile.injectFeaturedNote) return;
|
||||
}
|
||||
|
||||
const max = 30;
|
||||
const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
|
||||
|
||||
const query = Notes.createQueryBuilder('note')
|
||||
.addSelect('note.score')
|
||||
.where('note.userHost IS NULL')
|
||||
.andWhere(`note.score > 0`)
|
||||
.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
|
||||
.andWhere(`note.visibility = 'public'`)
|
||||
.innerJoinAndSelect('note.user', 'user');
|
||||
|
||||
if (user) {
|
||||
query.andWhere('note.userId != :userId', { userId: user.id });
|
||||
|
||||
generateMutedUserQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
|
||||
const reactionQuery = NoteReactions.createQueryBuilder('reaction')
|
||||
.select('reaction.noteId')
|
||||
.where('reaction.userId = :userId', { userId: user.id });
|
||||
|
||||
query.andWhere(`note.id NOT IN (${ reactionQuery.getQuery() })`);
|
||||
}
|
||||
|
||||
const notes = await query
|
||||
.orderBy('note.score', 'DESC')
|
||||
.take(max)
|
||||
.getMany();
|
||||
|
||||
if (notes.length === 0) return;
|
||||
|
||||
// Pick random one
|
||||
const featured = notes[Math.floor(Math.random() * notes.length)];
|
||||
|
||||
(featured as any)._featuredId_ = rndstr('a-z0-9', 8);
|
||||
|
||||
// Inject featured
|
||||
timeline.splice(3, 0, featured);
|
||||
}
|
34
packages/backend/src/server/api/common/inject-promo.ts
Normal file
34
packages/backend/src/server/api/common/inject-promo.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import rndstr from 'rndstr';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { PromoReads, PromoNotes, Notes, Users } from '@/models/index';
|
||||
|
||||
export async function injectPromo(timeline: Note[], user?: User | null) {
|
||||
if (timeline.length < 5) return;
|
||||
|
||||
// TODO: readやexpireフィルタはクエリ側でやる
|
||||
|
||||
const reads = user ? await PromoReads.find({
|
||||
userId: user.id
|
||||
}) : [];
|
||||
|
||||
let promos = await PromoNotes.find();
|
||||
|
||||
promos = promos.filter(n => n.expiresAt.getTime() > Date.now());
|
||||
promos = promos.filter(n => !reads.map(r => r.noteId).includes(n.noteId));
|
||||
|
||||
if (promos.length === 0) return;
|
||||
|
||||
// Pick random promo
|
||||
const promo = promos[Math.floor(Math.random() * promos.length)];
|
||||
|
||||
const note = await Notes.findOneOrFail(promo.noteId);
|
||||
|
||||
// Join
|
||||
note.user = await Users.findOneOrFail(note.userId);
|
||||
|
||||
(note as any)._prId_ = rndstr('a-z0-9', 8);
|
||||
|
||||
// Inject promo
|
||||
timeline.splice(3, 0, note);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export default (token: string) => token.length === 16;
|
@ -0,0 +1,28 @@
|
||||
import { SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
export function makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number) {
|
||||
if (sinceId && untilId) {
|
||||
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
|
||||
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
|
||||
q.orderBy(`${q.alias}.id`, 'DESC');
|
||||
} else if (sinceId) {
|
||||
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
|
||||
q.orderBy(`${q.alias}.id`, 'ASC');
|
||||
} else if (untilId) {
|
||||
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
|
||||
q.orderBy(`${q.alias}.id`, 'DESC');
|
||||
} else if (sinceDate && untilDate) {
|
||||
q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) });
|
||||
q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) });
|
||||
q.orderBy(`${q.alias}.createdAt`, 'DESC');
|
||||
} else if (sinceDate) {
|
||||
q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) });
|
||||
q.orderBy(`${q.alias}.createdAt`, 'ASC');
|
||||
} else if (untilDate) {
|
||||
q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) });
|
||||
q.orderBy(`${q.alias}.createdAt`, 'DESC');
|
||||
} else {
|
||||
q.orderBy(`${q.alias}.id`, 'DESC');
|
||||
}
|
||||
return q;
|
||||
}
|
122
packages/backend/src/server/api/common/read-messaging-message.ts
Normal file
122
packages/backend/src/server/api/common/read-messaging-message.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { publishMainStream, publishGroupMessagingStream } from '@/services/stream';
|
||||
import { publishMessagingStream } from '@/services/stream';
|
||||
import { publishMessagingIndexStream } from '@/services/stream';
|
||||
import { User, IRemoteUser } from '@/models/entities/user';
|
||||
import { MessagingMessage } from '@/models/entities/messaging-message';
|
||||
import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index';
|
||||
import { In } from 'typeorm';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error';
|
||||
import { UserGroup } from '@/models/entities/user-group';
|
||||
import { toArray } from '@/prelude/array';
|
||||
import { renderReadActivity } from '@/remote/activitypub/renderer/read';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index';
|
||||
import { deliver } from '@/queue/index';
|
||||
import orderedCollection from '@/remote/activitypub/renderer/ordered-collection';
|
||||
|
||||
/**
|
||||
* Mark messages as read
|
||||
*/
|
||||
export async function readUserMessagingMessage(
|
||||
userId: User['id'],
|
||||
otherpartyId: User['id'],
|
||||
messageIds: MessagingMessage['id'][]
|
||||
) {
|
||||
if (messageIds.length === 0) return;
|
||||
|
||||
const messages = await MessagingMessages.find({
|
||||
id: In(messageIds)
|
||||
});
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.recipientId !== userId) {
|
||||
throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).');
|
||||
}
|
||||
}
|
||||
|
||||
// Update documents
|
||||
await MessagingMessages.update({
|
||||
id: In(messageIds),
|
||||
userId: otherpartyId,
|
||||
recipientId: userId,
|
||||
isRead: false
|
||||
}, {
|
||||
isRead: true
|
||||
});
|
||||
|
||||
// Publish event
|
||||
publishMessagingStream(otherpartyId, userId, 'read', messageIds);
|
||||
publishMessagingIndexStream(userId, 'read', messageIds);
|
||||
|
||||
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||
publishMainStream(userId, 'readAllMessagingMessages');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark messages as read
|
||||
*/
|
||||
export async function readGroupMessagingMessage(
|
||||
userId: User['id'],
|
||||
groupId: UserGroup['id'],
|
||||
messageIds: MessagingMessage['id'][]
|
||||
) {
|
||||
if (messageIds.length === 0) return;
|
||||
|
||||
// check joined
|
||||
const joining = await UserGroupJoinings.findOne({
|
||||
userId: userId,
|
||||
userGroupId: groupId
|
||||
});
|
||||
|
||||
if (joining == null) {
|
||||
throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).');
|
||||
}
|
||||
|
||||
const messages = await MessagingMessages.find({
|
||||
id: In(messageIds)
|
||||
});
|
||||
|
||||
const reads: MessagingMessage['id'][] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.userId === userId) continue;
|
||||
if (message.reads.includes(userId)) continue;
|
||||
|
||||
// Update document
|
||||
await MessagingMessages.createQueryBuilder().update()
|
||||
.set({
|
||||
reads: (() => `array_append("reads", '${joining.userId}')`) as any
|
||||
})
|
||||
.where('id = :id', { id: message.id })
|
||||
.execute();
|
||||
|
||||
reads.push(message.id);
|
||||
}
|
||||
|
||||
// Publish event
|
||||
publishGroupMessagingStream(groupId, 'read', {
|
||||
ids: reads,
|
||||
userId: userId
|
||||
});
|
||||
publishMessagingIndexStream(userId, 'read', reads);
|
||||
|
||||
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||
publishMainStream(userId, 'readAllMessagingMessages');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deliverReadActivity(user: { id: User['id']; host: null; }, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) {
|
||||
messages = toArray(messages).filter(x => x.uri);
|
||||
const contents = messages.map(x => renderReadActivity(user, x));
|
||||
|
||||
if (contents.length > 1) {
|
||||
const collection = orderedCollection(null, contents.length, undefined, undefined, contents);
|
||||
deliver(user, renderActivity(collection), recipient.inbox);
|
||||
} else {
|
||||
for (const content of contents) {
|
||||
deliver(user, renderActivity(content), recipient.inbox);
|
||||
}
|
||||
}
|
||||
}
|
43
packages/backend/src/server/api/common/read-notification.ts
Normal file
43
packages/backend/src/server/api/common/read-notification.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { publishMainStream } from '@/services/stream';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Notification } from '@/models/entities/notification';
|
||||
import { Notifications, Users } from '@/models/index';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
export async function readNotification(
|
||||
userId: User['id'],
|
||||
notificationIds: Notification['id'][]
|
||||
) {
|
||||
// Update documents
|
||||
await Notifications.update({
|
||||
id: In(notificationIds),
|
||||
isRead: false
|
||||
}, {
|
||||
isRead: true
|
||||
});
|
||||
|
||||
post(userId);
|
||||
}
|
||||
|
||||
export async function readNotificationByQuery(
|
||||
userId: User['id'],
|
||||
query: Record<string, any>
|
||||
) {
|
||||
// Update documents
|
||||
await Notifications.update({
|
||||
...query,
|
||||
notifieeId: userId,
|
||||
isRead: false
|
||||
}, {
|
||||
isRead: true
|
||||
});
|
||||
|
||||
post(userId);
|
||||
}
|
||||
|
||||
async function post(userId: User['id']) {
|
||||
if (!await Users.getHasUnreadNotification(userId)) {
|
||||
// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
|
||||
publishMainStream(userId, 'readAllNotifications');
|
||||
}
|
||||
}
|
44
packages/backend/src/server/api/common/signin.ts
Normal file
44
packages/backend/src/server/api/common/signin.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import * as Koa from 'koa';
|
||||
|
||||
import config from '@/config/index';
|
||||
import { ILocalUser } from '@/models/entities/user';
|
||||
import { Signins } from '@/models/index';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
import { publishMainStream } from '@/services/stream';
|
||||
|
||||
export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
|
||||
if (redirect) {
|
||||
//#region Cookie
|
||||
ctx.cookies.set('igi', user.token, {
|
||||
path: '/',
|
||||
// SEE: https://github.com/koajs/koa/issues/974
|
||||
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
|
||||
secure: config.url.startsWith('https'),
|
||||
httpOnly: false
|
||||
});
|
||||
//#endregion
|
||||
|
||||
ctx.redirect(config.url);
|
||||
} else {
|
||||
ctx.body = {
|
||||
id: user.id,
|
||||
i: user.token
|
||||
};
|
||||
ctx.status = 200;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// Append signin history
|
||||
const record = await Signins.save({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: ctx.ip,
|
||||
headers: ctx.headers,
|
||||
success: true
|
||||
});
|
||||
|
||||
// Publish signin event
|
||||
publishMainStream(user.id, 'signin', await Signins.pack(record));
|
||||
})();
|
||||
}
|
113
packages/backend/src/server/api/common/signup.ts
Normal file
113
packages/backend/src/server/api/common/signup.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { generateKeyPair } from 'crypto';
|
||||
import generateUserToken from './generate-native-user-token';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Users, UsedUsernames } from '@/models/index';
|
||||
import { UserProfile } from '@/models/entities/user-profile';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
import { toPunyNullable } from '@/misc/convert-host';
|
||||
import { UserKeypair } from '@/models/entities/user-keypair';
|
||||
import { usersChart } from '@/services/chart/index';
|
||||
import { UsedUsername } from '@/models/entities/used-username';
|
||||
|
||||
export async function signup(opts: {
|
||||
username: User['username'];
|
||||
password?: string | null;
|
||||
passwordHash?: UserProfile['password'] | null;
|
||||
host?: string | null;
|
||||
}) {
|
||||
const { username, password, passwordHash, host } = opts;
|
||||
let hash = passwordHash;
|
||||
|
||||
// Validate username
|
||||
if (!Users.validateLocalUsername.ok(username)) {
|
||||
throw new Error('INVALID_USERNAME');
|
||||
}
|
||||
|
||||
if (password != null && passwordHash == null) {
|
||||
// Validate password
|
||||
if (!Users.validatePassword.ok(password)) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
hash = await bcrypt.hash(password, salt);
|
||||
}
|
||||
|
||||
// Generate secret
|
||||
const secret = generateUserToken();
|
||||
|
||||
// Check username duplication
|
||||
if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) {
|
||||
throw new Error('DUPLICATED_USERNAME');
|
||||
}
|
||||
|
||||
// Check deleted username duplication
|
||||
if (await UsedUsernames.findOne({ username: username.toLowerCase() })) {
|
||||
throw new Error('USED_USERNAME');
|
||||
}
|
||||
|
||||
const keyPair = await new Promise<string[]>((res, rej) =>
|
||||
generateKeyPair('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
cipher: undefined,
|
||||
passphrase: undefined
|
||||
}
|
||||
} as any, (err, publicKey, privateKey) =>
|
||||
err ? rej(err) : res([publicKey, privateKey])
|
||||
));
|
||||
|
||||
let account!: User;
|
||||
|
||||
// Start transaction
|
||||
await getConnection().transaction(async transactionalEntityManager => {
|
||||
const exist = await transactionalEntityManager.findOne(User, {
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: null
|
||||
});
|
||||
|
||||
if (exist) throw new Error(' the username is already used');
|
||||
|
||||
account = await transactionalEntityManager.save(new User({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
username: username,
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: toPunyNullable(host),
|
||||
token: secret,
|
||||
isAdmin: (await Users.count({
|
||||
host: null,
|
||||
})) === 0,
|
||||
}));
|
||||
|
||||
await transactionalEntityManager.save(new UserKeypair({
|
||||
publicKey: keyPair[0],
|
||||
privateKey: keyPair[1],
|
||||
userId: account.id
|
||||
}));
|
||||
|
||||
await transactionalEntityManager.save(new UserProfile({
|
||||
userId: account.id,
|
||||
autoAcceptFollowed: true,
|
||||
password: hash,
|
||||
}));
|
||||
|
||||
await transactionalEntityManager.save(new UsedUsername({
|
||||
createdAt: new Date(),
|
||||
username: username.toLowerCase(),
|
||||
}));
|
||||
});
|
||||
|
||||
usersChart.update(account, true);
|
||||
|
||||
return { account, secret };
|
||||
}
|
Reference in New Issue
Block a user