Fixed 267U.pre2
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { Blockings } from "../../../models/index.js";
|
||||
import { Brackets } from "typeorm";
|
||||
// ここでいうBlockedは被Blockedの意
|
||||
export function generateBlockedUserQuery(q, me) {
|
||||
const blockingQuery = Blockings.createQueryBuilder("blocking").select("blocking.blockerId").where("blocking.blockeeId = :blockeeId", {
|
||||
blockeeId: me.id
|
||||
}).andWhere("blocking.groupId IS NULL");
|
||||
const groupBlockingQuery = Blockings.createQueryBuilder("blocking").select("blocking.groupId").where("blocking.blockeeId = :groupBlockeeId", {
|
||||
groupBlockeeId: me.id
|
||||
}).andWhere("blocking.groupId IS NOT NULL");
|
||||
// 投稿の作者にブロックされていない かつ
|
||||
// 投稿の返信先の作者にブロックされていない かつ
|
||||
// 投稿の引用元の作者にブロックされていない
|
||||
q.andWhere(`note.userId NOT IN (${blockingQuery.getQuery()})`).andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.groupId IS NULL").orWhere(`note.groupId NOT IN (${groupBlockingQuery.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());
|
||||
q.setParameters(groupBlockingQuery.getParameters());
|
||||
}
|
||||
export function generateBlockQueryForUsers(q, me) {
|
||||
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,18 @@
|
||||
import { ChannelFollowings } from "../../../models/index.js";
|
||||
import { Brackets } from "typeorm";
|
||||
export function generateChannelQuery(q, me) {
|
||||
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,3 @@
|
||||
export function generateExcludeMemorietQuery(query) {
|
||||
query.andWhere(`NOT EXISTS (SELECT 1 FROM "memoriet" "memoriet_exclude" WHERE "memoriet_exclude"."noteId" = note.id)`);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Brackets } from "typeorm";
|
||||
import { Followings, Notes } from "../../../models/index.js";
|
||||
import { Cache } from "../../../misc/cache.js";
|
||||
import { apiLogger } from "../logger.js";
|
||||
export const cache = new Cache("homeTlQueryData", 60 * 60 * 24);
|
||||
const cutoff = 250; // 250 posts in the last 7 days, constant determined by comparing benchmarks for cutoff values between 100 and 2500
|
||||
const logger = apiLogger.createSubLogger("heuristics");
|
||||
export async function generateFollowingQuery(q, me) {
|
||||
const followingQuery = Followings.createQueryBuilder("following").select("following.followeeId").where("following.followerId = :meId");
|
||||
const heuristic = await cache.fetch(me.id, async ()=>{
|
||||
let curr = new Date();
|
||||
let prev = new Date();
|
||||
prev.setDate(prev.getDate() - 7);
|
||||
return Notes.createQueryBuilder('note').where(`note.createdAt > :prev`, {
|
||||
prev
|
||||
}).andWhere(`note.createdAt < :curr`, {
|
||||
curr
|
||||
}).andWhere(new Brackets((qb)=>{
|
||||
qb.where(`note.userId IN (${followingQuery.getQuery()})`);
|
||||
qb.orWhere(`note.userId = :meId`, {
|
||||
meId: me.id
|
||||
});
|
||||
})).getCount().then((res)=>{
|
||||
logger.info(`Calculating heuristics for user ${me.id} took ${new Date().getTime() - curr.getTime()}ms`);
|
||||
return res;
|
||||
});
|
||||
});
|
||||
const shouldUseUnion = heuristic < cutoff;
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
if (shouldUseUnion) {
|
||||
qb.where(`note.userId = ANY(array(${followingQuery.getQuery()} UNION ALL VALUES (:meId)))`);
|
||||
} else {
|
||||
qb.where(`note.userId = :meId`);
|
||||
qb.orWhere(`note.userId IN (${followingQuery.getQuery()})`);
|
||||
}
|
||||
}));
|
||||
q.setParameters({
|
||||
meId: me.id
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { Brackets } from "typeorm";
|
||||
import { sqlLikeEscape } from "../../../misc/sql-like-escape.js";
|
||||
import { sqlRegexEscape } from "../../../misc/sql-regex-escape.js";
|
||||
import { Followings, NoteFavorites, NoteReactions, Users } from "../../../models/index.js";
|
||||
const filters = {
|
||||
"from": fromFilter,
|
||||
"-from": fromFilterInverse,
|
||||
"mention": mentionFilter,
|
||||
"-mention": mentionFilterInverse,
|
||||
"reply": replyFilter,
|
||||
"-reply": replyFilterInverse,
|
||||
"to": replyFilter,
|
||||
"-to": replyFilterInverse,
|
||||
"before": beforeFilter,
|
||||
"until": beforeFilter,
|
||||
"after": afterFilter,
|
||||
"since": afterFilter,
|
||||
"instance": instanceFilter,
|
||||
"-instance": instanceFilterInverse,
|
||||
"domain": instanceFilter,
|
||||
"-domain": instanceFilterInverse,
|
||||
"host": instanceFilter,
|
||||
"-host": instanceFilterInverse,
|
||||
"filter": miscFilter,
|
||||
"-filter": miscFilterInverse,
|
||||
"in": inFilter,
|
||||
"-in": inFilterInverse,
|
||||
"has": attachmentFilter
|
||||
};
|
||||
export function generateFtsQuery(query, q) {
|
||||
const components = q.trim().split(" ");
|
||||
const terms = [];
|
||||
let finalTerms = [];
|
||||
let counter = 0;
|
||||
let caseSensitive = false;
|
||||
let matchWords = false;
|
||||
for (const component of components){
|
||||
const split = component.split(":");
|
||||
if (split.length > 1 && filters[split[0]] !== undefined) filters[split[0]](query, split.slice(1).join(":"), counter++);
|
||||
else if (split.length > 1 && (split[0] === "search" || split[0] === "match")) matchWords = split[1] === 'word' || split[1] === 'words';
|
||||
else if (split.length > 1 && split[0] === "case") caseSensitive = split[1] === 'sensitive';
|
||||
else terms.push(component);
|
||||
}
|
||||
let idx = 0;
|
||||
let state = 'idle';
|
||||
for(let i = 0; i < terms.length; i++){
|
||||
if (state === 'idle') {
|
||||
if (terms[i].startsWith('"') && terms[i].endsWith('"') || terms[i].startsWith('(') && terms[i].endsWith(')')) {
|
||||
finalTerms.push(trimStartAndEnd(terms[i]));
|
||||
} else if (terms[i].startsWith('"')) {
|
||||
idx = i;
|
||||
state = 'quote';
|
||||
} else if (terms[i].startsWith('(')) {
|
||||
idx = i;
|
||||
state = 'parenthesis';
|
||||
} else {
|
||||
finalTerms.push(terms[i]);
|
||||
}
|
||||
} else if (state === 'quote' && terms[i].endsWith('"')) {
|
||||
finalTerms.push(extractToken(terms, idx, i));
|
||||
state = 'idle';
|
||||
} else if (state === 'parenthesis' && terms[i].endsWith(')')) {
|
||||
query.andWhere(new Brackets((qb)=>{
|
||||
for (const term of extractToken(terms, idx, i).split(' OR ')){
|
||||
const id = counter++;
|
||||
appendSearchQuery(term, 'or', query, qb, id, term.startsWith('-'), matchWords, caseSensitive);
|
||||
}
|
||||
}));
|
||||
state = 'idle';
|
||||
}
|
||||
}
|
||||
if (state != "idle") {
|
||||
finalTerms.push(...extractToken(terms, idx, terms.length - 1, false).substring(1).split(' '));
|
||||
}
|
||||
for (const term of finalTerms){
|
||||
const id = counter++;
|
||||
appendSearchQuery(term, 'and', query, query, id, term.startsWith('-'), matchWords, caseSensitive);
|
||||
}
|
||||
}
|
||||
function fromFilter(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.userId = (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function fromFilterInverse(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.userId <> (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function mentionFilter(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.addCommonTableExpression(userQuery.getQuery(), `cte_${id}`, {
|
||||
materialized: true
|
||||
});
|
||||
query.andWhere(`note.mentions @> array[(SELECT * FROM cte_${id})]::varchar[]`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function mentionFilterInverse(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.addCommonTableExpression(userQuery.getQuery(), `cte_${id}`, {
|
||||
materialized: true
|
||||
});
|
||||
query.andWhere(`NOT (note.mentions @> array[(SELECT * FROM cte_${id})]::varchar[])`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function replyFilter(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.replyUserId = (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function replyFilterInverse(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.replyUserId <> (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function beforeFilter(query, filter) {
|
||||
query.andWhere('note.createdAt < :before', {
|
||||
before: filter
|
||||
});
|
||||
}
|
||||
function afterFilter(query, filter) {
|
||||
query.andWhere('note.createdAt > :after', {
|
||||
after: filter
|
||||
});
|
||||
}
|
||||
function instanceFilter(query, filter, id) {
|
||||
if (filter === 'local') {
|
||||
query.andWhere(`note.userHost IS NULL`);
|
||||
} else {
|
||||
query.andWhere(`note.userHost = :instance_${id}`);
|
||||
query.setParameter(`instance_${id}`, filter);
|
||||
}
|
||||
}
|
||||
function instanceFilterInverse(query, filter, id) {
|
||||
if (filter === 'local') {
|
||||
query.andWhere(`note.userHost IS NOT NULL`);
|
||||
} else {
|
||||
query.andWhere(`note.userHost <> :instance_${id}`);
|
||||
query.setParameter(`instance_${id}`, filter);
|
||||
}
|
||||
}
|
||||
function miscFilter(query, filter) {
|
||||
let subQuery = null;
|
||||
if (filter === 'followers') {
|
||||
subQuery = Followings.createQueryBuilder('following').select('following.followerId').where('following.followeeId = :meId');
|
||||
} else if (filter === 'following') {
|
||||
subQuery = Followings.createQueryBuilder('following').select('following.followeeId').where('following.followerId = :meId');
|
||||
} else if (filter === 'replies' || filter === 'reply') {
|
||||
query.andWhere('note.replyId IS NOT NULL');
|
||||
} else if (filter === 'boosts' || filter === 'boost' || filter === 'renotes' || filter === 'renote') {
|
||||
query.andWhere('note.renoteId IS NOT NULL');
|
||||
}
|
||||
if (subQuery !== null) query.andWhere(`note.userId IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
function miscFilterInverse(query, filter) {
|
||||
let subQuery = null;
|
||||
if (filter === 'followers') {
|
||||
subQuery = Followings.createQueryBuilder('following').select('following.followerId').where('following.followeeId = :meId');
|
||||
} else if (filter === 'following') {
|
||||
subQuery = Followings.createQueryBuilder('following').select('following.followeeId').where('following.followerId = :meId');
|
||||
} else if (filter === 'replies' || filter === 'reply') {
|
||||
query.andWhere('note.replyId IS NULL');
|
||||
} else if (filter === 'boosts' || filter === 'boost' || filter === 'renotes' || filter === 'renote') {
|
||||
query.andWhere('note.renoteId IS NULL');
|
||||
}
|
||||
if (subQuery !== null) query.andWhere(`note.userId NOT IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
function inFilter(query, filter) {
|
||||
let subQuery = null;
|
||||
if (filter === 'bookmarks') {
|
||||
subQuery = NoteFavorites.createQueryBuilder('bookmark').select('bookmark.noteId').where('bookmark.userId = :meId');
|
||||
} else if (filter === 'favorites' || filter === 'favourites' || filter === 'reactions' || filter === 'likes') {
|
||||
subQuery = NoteReactions.createQueryBuilder('react').select('react.noteId').where('react.userId = :meId');
|
||||
}
|
||||
if (subQuery !== null) query.andWhere(`note.id IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
function inFilterInverse(query, filter) {
|
||||
let subQuery = null;
|
||||
if (filter === 'bookmarks') {
|
||||
subQuery = NoteFavorites.createQueryBuilder('bookmark').select('bookmark.noteId').where('bookmark.userId = :meId');
|
||||
} else if (filter === 'favorites' || filter === 'favourites' || filter === 'reactions' || filter === 'likes') {
|
||||
subQuery = NoteReactions.createQueryBuilder('react').select('react.noteId').where('react.userId = :meId');
|
||||
}
|
||||
if (subQuery !== null) query.andWhere(`note.id NOT IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
function attachmentFilter(query, filter) {
|
||||
switch(filter){
|
||||
case 'image':
|
||||
query.andWhere(`note."attachedFileTypes"::varchar ILIKE '%image/%'`);
|
||||
break;
|
||||
case 'video':
|
||||
query.andWhere(`note."attachedFileTypes"::varchar ILIKE '%video/%'`);
|
||||
break;
|
||||
case 'audio':
|
||||
query.andWhere(`note."attachedFileTypes"::varchar ILIKE '%audio/%'`);
|
||||
break;
|
||||
case 'file':
|
||||
query.andWhere(`note."attachedFileTypes" <> '{}'`);
|
||||
query.andWhere(`NOT (note."attachedFileTypes"::varchar ILIKE '%image/%')`);
|
||||
query.andWhere(`NOT (note."attachedFileTypes"::varchar ILIKE '%video/%')`);
|
||||
query.andWhere(`NOT (note."attachedFileTypes"::varchar ILIKE '%audio/%')`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
function generateUserSubquery(filter, id) {
|
||||
if (filter.startsWith('@')) filter = filter.substring(1);
|
||||
const split = filter.split('@');
|
||||
const query = Users.createQueryBuilder('user').select('user.id').where(`user.usernameLower = :user_${id}`).andWhere(`user.host ${split[1] !== undefined ? `= :host_${id}` : 'IS NULL'}`);
|
||||
query.setParameter(`user_${id}`, split[0].toLowerCase());
|
||||
if (split[1] !== undefined) query.setParameter(`host_${id}`, split[1].toLowerCase());
|
||||
return query;
|
||||
}
|
||||
function extractToken(array, start, end, trim = true) {
|
||||
const slice = array.slice(start, end + 1).join(" ");
|
||||
return trim ? trimStartAndEnd(slice) : slice;
|
||||
}
|
||||
function trimStartAndEnd(str) {
|
||||
return str.substring(1, str.length - 1);
|
||||
}
|
||||
function appendSearchQuery(term, mode, query, qb, id, negate, matchWords, caseSensitive) {
|
||||
const sql = `note.text ${getSearchMatchOperator(negate, matchWords, caseSensitive)} :q_${id}`;
|
||||
if (mode === 'and') qb.andWhere(sql);
|
||||
else if (mode === 'or') qb.orWhere(sql);
|
||||
query.setParameter(`q_${id}`, escapeSqlSearchParam(term.substring(negate ? 1 : 0), matchWords));
|
||||
}
|
||||
function getSearchMatchOperator(negate, matchWords, caseSensitive) {
|
||||
const negatePrefix = matchWords ? '!' : 'NOT ';
|
||||
return `${negate ? negatePrefix : ''}${matchWords ? caseSensitive ? '~' : '~*' : caseSensitive ? 'LIKE' : 'ILIKE'}`;
|
||||
}
|
||||
function escapeSqlSearchParam(param, matchWords) {
|
||||
return matchWords ? `\\y${sqlRegexEscape(param)}\\y` : `%${sqlLikeEscape(param)}%`;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Brackets } from "typeorm";
|
||||
import { UserListJoinings, UserLists } from "../../../models/index.js";
|
||||
export function generateListQuery(q, me) {
|
||||
const listQuery = UserLists.createQueryBuilder("list").select("list.id").where("list.hideFromHomeTl = TRUE").andWhere("list.userId = :meId");
|
||||
const memberQuery = UserListJoinings.createQueryBuilder("member").select("member.userId").where(`member.userListId IN (${listQuery.getQuery()})`);
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`note.userId = :meId`);
|
||||
qb.orWhere(`note.userId NOT IN (${memberQuery.getQuery()})`);
|
||||
}));
|
||||
q.setParameters({
|
||||
meId: me.id
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Brackets } from "typeorm";
|
||||
export function shouldHideEUsersFor(viewer) {
|
||||
return !!viewer && !viewer.isAdmin && !viewer.isModerator && (viewer.minorBadges ?? []).some((badge)=>badge === "K" || badge === "T");
|
||||
}
|
||||
export function generateMinorBadgeUserVisibilityQuery(q, viewer, alias = "user") {
|
||||
if (!shouldHideEUsersFor(viewer)) return;
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`${alias}.id = :minorBadgeViewerId`).orWhere(`NOT ('E' = ANY(${alias}."minorBadges"))`);
|
||||
}));
|
||||
q.setParameter("minorBadgeViewerId", viewer.id);
|
||||
}
|
||||
export function generateMinorBadgeNoteVisibilityQuery(q, viewer, noteAlias = "note") {
|
||||
if (!shouldHideEUsersFor(viewer)) return;
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`${noteAlias}."userId" = :minorBadgeViewerId`).orWhere(`${noteAlias}."userId" NOT IN (` + `SELECT "id" FROM "user" WHERE 'E' = ANY("minorBadges")` + `)`);
|
||||
}));
|
||||
q.setParameter("minorBadgeViewerId", viewer.id);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NoteThreadMutings } from "../../../models/index.js";
|
||||
import { Brackets } from "typeorm";
|
||||
export function generateMutedNoteThreadQuery(q, me) {
|
||||
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,39 @@
|
||||
import { Brackets } from "typeorm";
|
||||
import { Mutings, UserProfiles } from "../../../models/index.js";
|
||||
export function generateMutedUserQuery(q, me, exclude) {
|
||||
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
|
||||
});
|
||||
}
|
||||
const mutingInstanceQuery = UserProfiles.createQueryBuilder("user_profile").select("user_profile.mutedInstances").where("user_profile.userId = :muterId", {
|
||||
muterId: me.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()})`);
|
||||
}))// mute instances
|
||||
.andWhere(new Brackets((qb)=>{
|
||||
qb.andWhere("note.userHost IS NULL").orWhere(`NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.userHost)`);
|
||||
})).andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.replyUserHost IS NULL").orWhere(`NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.replyUserHost)`);
|
||||
})).andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.renoteUserHost IS NULL").orWhere(`NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.renoteUserHost)`);
|
||||
}));
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
q.setParameters(mutingInstanceQuery.getParameters());
|
||||
}
|
||||
export function generateMutedUserQueryForUsers(q, me) {
|
||||
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,2 @@
|
||||
import { secureRndstr } from "../../../misc/secure-rndstr.js";
|
||||
export default (()=>secureRndstr(16));
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Brackets } from "typeorm";
|
||||
export function generateRepliesQuery(q, withReplies, me) {
|
||||
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 if (!withReplies) {
|
||||
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,43 @@
|
||||
import { Followings } from "../../../models/index.js";
|
||||
import { Brackets } from "typeorm";
|
||||
import { generateMinorBadgeNoteVisibilityQuery } from "./generate-minor-badge-visibility-query.js";
|
||||
export function generateVisibilityQuery(q, me, options) {
|
||||
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
|
||||
if (me == null) {
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`note.visibility = 'public'`).orWhere(`note.visibility = 'home'`);
|
||||
})).andWhere('note.localOnly = FALSE');
|
||||
} else {
|
||||
const followingQuery = Followings.createQueryBuilder("following").select("following.followeeId").where("following.followerId = :meId");
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb// 公開投稿である
|
||||
.where(new Brackets((qb)=>{
|
||||
qb.where(`note.visibility = 'public'`).orWhere(`note.visibility = 'home'`);
|
||||
}))// または 自分自身
|
||||
.orWhere("note.userId = :meId")// または 自分宛て
|
||||
.orWhere(":meId = ANY(note.visibleUserIds)").orWhere(":meId = ANY(note.mentions)").orWhere(new Brackets((qb)=>{
|
||||
qb// または フォロワー宛ての投稿であり、
|
||||
.where(`note.visibility = 'followers'`).andWhere(new Brackets((qb)=>{
|
||||
qb// 自分がフォロワーである
|
||||
.where(`note.userId IN (${followingQuery.getQuery()})`)// または 自分の投稿へのリプライ
|
||||
.orWhere("note.replyUserId = :meId");
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`note.visibility != 'hidden'`).orWhere(`note.userId = :meId`);
|
||||
}));
|
||||
q.setParameters({
|
||||
meId: me.id
|
||||
});
|
||||
}
|
||||
if (!options?.allowAdservice) {
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`NOT ('adservice' = ANY(note.tags))`);
|
||||
if (me) {
|
||||
qb.orWhere("note.userId = :meId");
|
||||
}
|
||||
}));
|
||||
}
|
||||
generateMinorBadgeNoteVisibilityQuery(q, me);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Brackets } from "typeorm";
|
||||
import { RenoteMutings } from "../../../models/index.js";
|
||||
export function generateMutedUserRenotesQueryForNotes(q, me) {
|
||||
const mutingQuery = RenoteMutings.createQueryBuilder("renote_muting").select("renote_muting.muteeId").where("renote_muting.muterId = :muterId", {
|
||||
muterId: me.id
|
||||
});
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(new Brackets((qb)=>{
|
||||
qb.where("note.renoteId IS NOT NULL");
|
||||
qb.andWhere("note.text IS NULL");
|
||||
qb.andWhere(`note.userId NOT IN (${mutingQuery.getQuery()})`);
|
||||
})).orWhere("note.renoteId IS NULL").orWhere("note.text IS NOT NULL");
|
||||
}));
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { UserGroupJoinings, UserGroups } from "../../../models/index.js";
|
||||
export async function getGroupActor(groupId, user) {
|
||||
if (groupId == null) return null;
|
||||
const group = await UserGroups.findOneBy({
|
||||
id: groupId
|
||||
});
|
||||
if (group == null) return null;
|
||||
if (group.userId === user.id) return group;
|
||||
const joining = await UserGroupJoinings.findOneBy({
|
||||
userId: user.id,
|
||||
userGroupId: group.id
|
||||
});
|
||||
return joining == null ? null : group;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { IdentifiableError } from "../../../misc/identifiable-error.js";
|
||||
import { Notes, Users } from "../../../models/index.js";
|
||||
import { generateVisibilityQuery } from "./generate-visibility-query.js";
|
||||
/**
|
||||
* Get note for API processing, taking into account visibility.
|
||||
*/ export async function getNote(noteId, me, options) {
|
||||
const query = Notes.createQueryBuilder("note").where("note.id = :id", {
|
||||
id: noteId
|
||||
});
|
||||
generateVisibilityQuery(query, me, options);
|
||||
const note = await query.getOne();
|
||||
if (note == null || me == null && note.localOnly) {
|
||||
throw new IdentifiableError("9725d0ce-ba28-4dde-95a7-2cbb2c15de24", "No such note.");
|
||||
}
|
||||
return note;
|
||||
}
|
||||
/**
|
||||
* Get user for API processing
|
||||
*/ export async function getUser(userId) {
|
||||
const user = await Users.findOneBy({
|
||||
id: 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) {
|
||||
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) {
|
||||
const user = await getUser(userId);
|
||||
if (!Users.isLocalUser(user)) {
|
||||
throw new Error("user is not a local user");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import rndstr from "rndstr";
|
||||
import { Notes, UserProfiles, NoteReactions } from "../../../models/index.js";
|
||||
import { generateMutedUserQuery } from "./generate-muted-user-query.js";
|
||||
import { generateBlockedUserQuery } from "./generate-block-query.js";
|
||||
// TODO: リアクション、Renote、返信などをしたノートは除外する
|
||||
export async function injectFeatured(timeline, user) {
|
||||
if (timeline.length < 5) return;
|
||||
if (user) {
|
||||
const profile = await UserProfiles.findOneByOrFail({
|
||||
userId: 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._featuredId_ = rndstr("a-z0-9", 8);
|
||||
// Inject featured
|
||||
timeline.splice(3, 0, featured);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import rndstr from "rndstr";
|
||||
import { PromoReads, PromoNotes, Notes, Users } from "../../../models/index.js";
|
||||
import { shouldHideEUsersFor } from "./generate-minor-badge-visibility-query.js";
|
||||
import { readPromo } from "./read-promo.js";
|
||||
const systemTags = new Set([
|
||||
"adservice",
|
||||
"videoservice",
|
||||
"audioservice",
|
||||
"imageservice",
|
||||
"karaokeservice",
|
||||
"lua4frozen"
|
||||
]);
|
||||
function isExplicitAd(note) {
|
||||
return note.tags.includes("explicit") || note.user?.minorBadges?.includes("E") === true;
|
||||
}
|
||||
export async function injectPromo(timeline, user, preferredTag) {
|
||||
// TODO: readやexpireフィルタはクエリ側でやる
|
||||
const readDay = new Date().toISOString().slice(0, 10);
|
||||
const reads = user ? await PromoReads.findBy({
|
||||
userId: user.id,
|
||||
readDay
|
||||
}) : [];
|
||||
let promos = await PromoNotes.find();
|
||||
promos = promos.filter((n)=>n.expiresAt.getTime() > Date.now());
|
||||
promos = promos.filter((n)=>n.remainingCredits > 0);
|
||||
promos = promos.filter((n)=>!reads.map((r)=>r.noteId).includes(n.noteId));
|
||||
if (promos.length === 0) return;
|
||||
const candidates = [];
|
||||
for (const promo of promos){
|
||||
const note = await Notes.findOneBy({
|
||||
id: promo.noteId
|
||||
});
|
||||
if (!note?.tags.includes("adservice")) continue;
|
||||
note.user = await Users.findOneByOrFail({
|
||||
id: note.userId
|
||||
});
|
||||
if (user && shouldHideEUsersFor(user) && isExplicitAd(note)) continue;
|
||||
candidates.push(note);
|
||||
}
|
||||
if (candidates.length === 0) return;
|
||||
const normalizedPreferredTag = preferredTag?.trim().toLowerCase().replace(/^#/, "");
|
||||
const priority = normalizedPreferredTag && !systemTags.has(normalizedPreferredTag) ? candidates.filter((note)=>note.tags.includes(normalizedPreferredTag)) : [];
|
||||
const pool = priority.length > 0 ? priority : candidates;
|
||||
// Pick random promo
|
||||
const note = pool[Math.floor(Math.random() * pool.length)];
|
||||
const promo = promos.find((promo)=>promo.noteId === note.id);
|
||||
note._prId_ = rndstr("a-z0-9", 8);
|
||||
if (user && promo) {
|
||||
await readPromo(note, promo, user);
|
||||
}
|
||||
// Inject promo
|
||||
timeline.splice(Math.min(3, timeline.length), 0, note);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default ((token)=>token.length === 16);
|
||||
@@ -0,0 +1,42 @@
|
||||
export function makePaginationQuery(q, sinceId, untilId, sinceDate, untilDate) {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { publishMainStream, publishGroupMessagingStream } from "../../../services/stream.js";
|
||||
import { publishMessagingStream } from "../../../services/stream.js";
|
||||
import { publishMessagingIndexStream } from "../../../services/stream.js";
|
||||
import { pushNotification } from "../../../services/push-notification.js";
|
||||
import { MessagingMessages, UserGroupJoinings, Users } from "../../../models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { IdentifiableError } from "../../../misc/identifiable-error.js";
|
||||
import { toArray } from "../../../prelude/array.js";
|
||||
import { renderReadActivity } from "../../../remote/activitypub/renderer/read.js";
|
||||
import { renderActivity } from "../../../remote/activitypub/renderer/index.js";
|
||||
import { deliver } from "../../../queue/index.js";
|
||||
import orderedCollection from "../../../remote/activitypub/renderer/ordered-collection.js";
|
||||
/**
|
||||
* Mark messages as read
|
||||
*/ export async function readUserMessagingMessage(userId, otherpartyId, messageIds) {
|
||||
if (messageIds.length === 0) return;
|
||||
const messages = await MessagingMessages.findBy({
|
||||
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");
|
||||
pushNotification(userId, "readAllMessagingMessages", undefined);
|
||||
} else {
|
||||
// そのユーザーとのメッセージで未読がなければイベント発行
|
||||
const count = await MessagingMessages.count({
|
||||
where: {
|
||||
userId: otherpartyId,
|
||||
recipientId: userId,
|
||||
isRead: false
|
||||
},
|
||||
take: 1
|
||||
});
|
||||
if (!count) {
|
||||
pushNotification(userId, "readAllMessagingMessagesOfARoom", {
|
||||
userId: otherpartyId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Mark messages as read
|
||||
*/ export async function readGroupMessagingMessage(userId, groupId, messageIds) {
|
||||
if (messageIds.length === 0) return;
|
||||
// check joined
|
||||
const joining = await UserGroupJoinings.findOneBy({
|
||||
userId: userId,
|
||||
userGroupId: groupId
|
||||
});
|
||||
if (joining == null) {
|
||||
throw new IdentifiableError("930a270c-714a-46b2-b776-ad27276dc569", "Access denied (group).");
|
||||
}
|
||||
const messages = await MessagingMessages.findBy({
|
||||
id: In(messageIds)
|
||||
});
|
||||
const reads = [];
|
||||
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}')`
|
||||
}).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");
|
||||
pushNotification(userId, "readAllMessagingMessages", undefined);
|
||||
} else {
|
||||
// そのグループにおいて未読がなければイベント発行
|
||||
const unreadExist = await MessagingMessages.createQueryBuilder("message").where("message.groupId = :groupId", {
|
||||
groupId: groupId
|
||||
}).andWhere("message.userId != :userId", {
|
||||
userId: userId
|
||||
}).andWhere("NOT (:userId = ANY(message.reads))", {
|
||||
userId: userId
|
||||
}).andWhere("message.createdAt > :joinedAt", {
|
||||
joinedAt: joining.createdAt
|
||||
}) // 自分が加入する前の会話については、未読扱いしない
|
||||
.getOne().then((x)=>x != null);
|
||||
if (!unreadExist) {
|
||||
pushNotification(userId, "readAllMessagingMessagesOfARoom", {
|
||||
groupId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function deliverReadActivity(user, recipient, messages) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { In } from "typeorm";
|
||||
import { publishMainStream } from "../../../services/stream.js";
|
||||
import { pushNotification } from "../../../services/push-notification.js";
|
||||
import { Notifications, Users } from "../../../models/index.js";
|
||||
export async function readNotification(userId, notificationIds) {
|
||||
if (notificationIds.length === 0) return;
|
||||
// Update documents
|
||||
const result = await Notifications.update({
|
||||
notifieeId: userId,
|
||||
id: In(notificationIds),
|
||||
isRead: false
|
||||
}, {
|
||||
isRead: true
|
||||
});
|
||||
if (result.affected === 0) return;
|
||||
if (!await Users.getHasUnreadNotification(userId)) return postReadAllNotifications(userId);
|
||||
else return postReadNotifications(userId, notificationIds);
|
||||
}
|
||||
export async function readNotificationByQuery(userId, query) {
|
||||
const notificationIds = await Notifications.findBy({
|
||||
...query,
|
||||
notifieeId: userId,
|
||||
isRead: false
|
||||
}).then((notifications)=>notifications.map((notification)=>notification.id));
|
||||
return readNotification(userId, notificationIds);
|
||||
}
|
||||
function postReadAllNotifications(userId) {
|
||||
publishMainStream(userId, "readAllNotifications");
|
||||
return pushNotification(userId, "readAllNotifications", undefined);
|
||||
}
|
||||
function postReadNotifications(userId, notificationIds) {
|
||||
publishMainStream(userId, "readNotifications", notificationIds);
|
||||
return pushNotification(userId, "readNotifications", {
|
||||
notificationIds
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { PromoNotes, PromoReads } from "../../../models/index.js";
|
||||
import { genId } from "../../../misc/gen-id.js";
|
||||
export async function readPromo(note, promo, user) {
|
||||
if (promo.expiresAt.getTime() <= Date.now() || promo.remainingCredits <= 0 || promo.userId === user.id || note.userId === user.id || user.isAdmin || user.isModerator || user.isBot) {
|
||||
return;
|
||||
}
|
||||
const readDay = new Date().toISOString().slice(0, 10);
|
||||
const result = await PromoReads.createQueryBuilder().insert().values({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
readDay
|
||||
}).orIgnore().returning("id").execute();
|
||||
if (result.raw.length === 0) {
|
||||
return;
|
||||
}
|
||||
await PromoNotes.createQueryBuilder().update().set({
|
||||
remainingCredits: ()=>`"remainingCredits" - 1`
|
||||
}).where(`"noteId" = :noteId`, {
|
||||
noteId: note.id
|
||||
}).andWhere(`"remainingCredits" > 0`).execute();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import config from "../../../config/index.js";
|
||||
import { Signins } from "../../../models/index.js";
|
||||
import { genId } from "../../../misc/gen-id.js";
|
||||
import { publishMainStream } from "../../../services/stream.js";
|
||||
export default function(ctx, user, 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.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: ctx.ip,
|
||||
headers: ctx.headers,
|
||||
success: true
|
||||
}).then((x)=>Signins.findOneByOrFail(x.identifiers[0]));
|
||||
// Publish signin event
|
||||
publishMainStream(user.id, "signin", await Signins.pack(record));
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { generateKeyPair } from "node:crypto";
|
||||
import generateUserToken from "./generate-native-user-token.js";
|
||||
import { User } from "../../../models/entities/user.js";
|
||||
import { Users, UsedUsernames } from "../../../models/index.js";
|
||||
import { UserProfile } from "../../../models/entities/user-profile.js";
|
||||
import { IsNull } from "typeorm";
|
||||
import { genId } from "../../../misc/gen-id.js";
|
||||
import { toPunyNullable } from "../../../misc/convert-host.js";
|
||||
import { UserKeypair } from "../../../models/entities/user-keypair.js";
|
||||
import { usersChart } from "../../../services/chart/index.js";
|
||||
import { UsedUsername } from "../../../models/entities/used-username.js";
|
||||
import { db } from "../../../db/postgre.js";
|
||||
import config from "../../../config/index.js";
|
||||
import { hashPassword } from "../../../misc/password.js";
|
||||
import { fetchMeta } from "../../../misc/fetch-meta.js";
|
||||
import follow from "../../../services/following/create.js";
|
||||
export async function signup(opts) {
|
||||
const { username, password, passwordHash, host } = opts;
|
||||
let hash = passwordHash;
|
||||
const userCount = await Users.countBy({
|
||||
host: IsNull()
|
||||
});
|
||||
if (config.maxUserSignups != null && userCount > config.maxUserSignups) {
|
||||
throw new Error("MAX_USERS_REACHED");
|
||||
}
|
||||
// Validate username
|
||||
if (!Users.validateLocalUsername(username)) {
|
||||
throw new Error("INVALID_USERNAME");
|
||||
}
|
||||
if (password != null && passwordHash == null) {
|
||||
// Validate password
|
||||
if (!Users.validatePassword(password)) {
|
||||
throw new Error("INVALID_PASSWORD");
|
||||
}
|
||||
// Generate hash of password
|
||||
hash = await hashPassword(password);
|
||||
}
|
||||
// Generate secret
|
||||
const secret = generateUserToken();
|
||||
// Check username duplication
|
||||
if (await Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull()
|
||||
})) {
|
||||
throw new Error("DUPLICATED_USERNAME");
|
||||
}
|
||||
// Check deleted username duplication
|
||||
if (await UsedUsernames.findOneBy({
|
||||
username: username.toLowerCase()
|
||||
})) {
|
||||
throw new Error("USED_USERNAME");
|
||||
}
|
||||
const keyPair = await new Promise((res, rej)=>generateKeyPair("rsa", {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: "spki",
|
||||
format: "pem"
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8",
|
||||
format: "pem",
|
||||
cipher: undefined,
|
||||
passphrase: undefined
|
||||
}
|
||||
}, (err, publicKey, privateKey)=>err ? rej(err) : res([
|
||||
publicKey,
|
||||
privateKey
|
||||
])));
|
||||
const exist = await Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull()
|
||||
});
|
||||
if (exist) throw new Error("The username is already in use");
|
||||
// Prepare objects
|
||||
const user = new User({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
username: username,
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: toPunyNullable(host),
|
||||
token: secret,
|
||||
isAdmin: await Users.countBy({
|
||||
host: IsNull(),
|
||||
isAdmin: true
|
||||
}) === 0
|
||||
});
|
||||
const userKeypair = new UserKeypair({
|
||||
publicKey: keyPair[0],
|
||||
privateKey: keyPair[1],
|
||||
userId: user.id
|
||||
});
|
||||
const userProfile = new UserProfile({
|
||||
userId: user.id,
|
||||
autoAcceptFollowed: true,
|
||||
allowCalls: false,
|
||||
password: hash
|
||||
});
|
||||
const usedUsername = new UsedUsername({
|
||||
createdAt: new Date(),
|
||||
username: username.toLowerCase()
|
||||
});
|
||||
// Save the objects atomically using a db transaction, note that we should never run any code in a transaction block directly
|
||||
await db.transaction(async (transactionalEntityManager)=>{
|
||||
await transactionalEntityManager.save(user);
|
||||
await transactionalEntityManager.save(userKeypair);
|
||||
await transactionalEntityManager.save(userProfile);
|
||||
await transactionalEntityManager.save(usedUsername);
|
||||
});
|
||||
const account = await Users.findOneByOrFail({
|
||||
id: user.id
|
||||
});
|
||||
const meta = await fetchMeta();
|
||||
// If an autofollow account exists, follow it
|
||||
if (meta.autofollowedAccount) {
|
||||
const autofollowedAccount = await Users.findOneByOrFail({
|
||||
usernameLower: meta.autofollowedAccount.toLowerCase(),
|
||||
host: IsNull()
|
||||
});
|
||||
if (autofollowedAccount) {
|
||||
await follow(account, autofollowedAccount);
|
||||
}
|
||||
}
|
||||
usersChart.update(account, true);
|
||||
return {
|
||||
account,
|
||||
secret
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user