adjusting schema

This commit is contained in:
Namekuji 2023-07-25 07:28:08 -04:00
parent 168001271f
commit b211788d3c
No known key found for this signature in database
GPG key ID: 1D62332C07FBA532
10 changed files with 147 additions and 38 deletions

View file

@ -42,9 +42,9 @@ CREATE TABLE IF NOT EXISTS note ( -- Models timeline
"url" text, "url" text,
"score" int, "score" int,
"files" set<frozen<drive_file>>, "files" set<frozen<drive_file>>,
"visibleUsersId" set<ascii>, "visibleUserIds" set<ascii>,
"mentions" set<ascii>, "mentions" set<ascii>,
"emojis" set<frozen<emoji>>, "emojis" set<text>,
"tags" set<text>, "tags" set<text>,
"hasPoll" boolean, "hasPoll" boolean,
"threadId" ascii, "threadId" ascii,
@ -54,7 +54,6 @@ CREATE TABLE IF NOT EXISTS note ( -- Models timeline
"replyId" ascii, -- Reply "replyId" ascii, -- Reply
"renoteId" ascii, -- Boost "renoteId" ascii, -- Boost
"reactions" map<text, int>, "reactions" map<text, int>,
"reactionEmojis" map<text, frozen<emoji>>,
"noteEdit" set<frozen<note_edit_history>>, -- Edit History "noteEdit" set<frozen<note_edit_history>>, -- Edit History
"updatedAt" timestamp, "updatedAt" timestamp,
PRIMARY KEY ("createdAtDate", "createdAt", "id") PRIMARY KEY ("createdAtDate", "createdAt", "id")

View file

@ -1,5 +1,6 @@
import config from "@/config/index.js"; import config from "@/config/index.js";
import type { PopulatedEmoji } from "@/misc/populate-emojis.js"; import type { PopulatedEmoji } from "@/misc/populate-emojis.js";
import type { Note } from "@/models/entities/note.js";
import type { NoteReaction } from "@/models/entities/note-reaction.js"; import type { NoteReaction } from "@/models/entities/note-reaction.js";
import { Client, types } from "cassandra-driver"; import { Client, types } from "cassandra-driver";
@ -34,7 +35,7 @@ export const prepared = {
"url", "url",
"score", "score",
"files", "files",
"visibleUsersId", "visibleUserIds",
"mentions", "mentions",
"emojis", "emojis",
"tags", "tags",
@ -46,12 +47,11 @@ export const prepared = {
"replyId", "replyId",
"renoteId", "renoteId",
"reactions", "reactions",
"reactionEmojis",
"noteEdit", "noteEdit",
"updatedAt" "updatedAt"
) )
VALUES VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
select: { select: {
byDate: `SELECT * FROM note WHERE "createdAtDate" IN ?`, byDate: `SELECT * FROM note WHERE "createdAtDate" IN ?`,
byId: `SELECT * FROM note WHERE "id" IN ?`, byId: `SELECT * FROM note WHERE "id" IN ?`,
@ -59,10 +59,17 @@ export const prepared = {
byUrl: `SELECT * FROM note WHERE "url" IN ?`, byUrl: `SELECT * FROM note WHERE "url" IN ?`,
byUserId: `SELECT * FROM note_by_userid WHERE "userId" IN ?`, byUserId: `SELECT * FROM note_by_userid WHERE "userId" IN ?`,
}, },
delete: `DELETE FROM note WHERE "createdAtDate" = ? AND "createdAt" = ?`, delete: `DELETE FROM note WHERE "createdAtDate" = ? AND "createdAt" = ? AND "id" = ?`,
update: { update: {
renoteCount: `UPDATE note SET "renoteCount" = ?, "score" = ? WHERE "createdAtDate" = ? AND "createdAt" = ? IF EXISTS`, renoteCount: `UPDATE note SET
reactions: `UPDATE note SET "reactions" = ?, "score" = ? WHERE "createdAtDate" = ? AND "createdAt" = ? IF EXISTS`, "renoteCount" = ?,
"score" = ?
WHERE "createdAtDate" = ? AND "createdAt" = ? AND "id" = ? IF EXISTS`,
reactions: `UPDATE note SET
"emojis" = ?,
"reactions" = ?,
"score" = ?
WHERE "createdAtDate" = ? AND "createdAt" = ? AND "id" = ? IF EXISTS`,
}, },
}, },
reaction: { reaction: {
@ -92,12 +99,60 @@ export interface ScyllaDriveFile {
isLink: boolean; isLink: boolean;
md5: string; md5: string;
size: number; size: number;
width: number;
height: number;
} }
export type ScyllaNoteReaction = NoteReaction & { export interface ScyllaNoteEditHistory {
emoji: PopulatedEmoji content: string;
cw: string;
files: ScyllaDriveFile[];
updatedAt: Date;
}
export type ScyllaNote = Partial<Note> & {
createdAtDate: Date;
files: ScyllaDriveFile[];
channelName: string;
noteEdit: ScyllaNoteEditHistory[];
};
export function parseScyllaNote(row: types.Row): ScyllaNote {
const files: ScyllaDriveFile[] = row.get("files");
return {
createdAtDate: row.get("createdAtDate"),
createdAt: row.get("createdAt"),
id: row.get("id"),
visibility: row.get("visibility"),
text: row.get("content"),
name: row.get("name"),
cw: row.get("cw"),
localOnly: row.get("localOnly"),
renoteCount: row.get("renoteCount"),
repliesCount: row.get("repliesCount"),
uri: row.get("uri"),
url: row.get("url"),
score: row.get("score"),
files,
fileIds: files.map((file) => file.id),
attachedFileTypes: files.map((file) => file.type),
visibleUserIds: row.get("visibleUserIds"),
mentions: row.get("mentions"),
emojis: row.get("emojis"),
tags: row.get("tags"),
hasPoll: row.get("hasPoll"),
threadId: row.get("threadId"),
channelId: row.get("channelId"),
channelName: row.get("channelName"),
userId: row.get("userId"),
replyId: row.get("replyId"),
renoteId: row.get("replyId"),
reactions: row.get("reactions"),
noteEdit: row.get("noteEdit"),
updatedAt: row.get("updatedAt"),
}
}
export interface ScyllaNoteReaction extends NoteReaction {
emoji: PopulatedEmoji;
} }
export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction { export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction {
@ -108,5 +163,5 @@ export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction {
reaction: row.get("reaction"), reaction: row.get("reaction"),
createdAt: row.get("createdAt"), createdAt: row.get("createdAt"),
emoji: row.get("emoji"), emoji: row.get("emoji"),
} };
} }

View file

@ -9,7 +9,10 @@ import config from "@/config/index.js";
import { query } from "@/prelude/url.js"; import { query } from "@/prelude/url.js";
import { redisClient } from "@/db/redis.js"; import { redisClient } from "@/db/redis.js";
export const EmojiCache = new Cache<Emoji | null>("populateEmojis", 60 * 60 * 12); export const EmojiCache = new Cache<Emoji | null>(
"populateEmojis",
60 * 60 * 12,
);
/** /**
* *
@ -71,13 +74,12 @@ export async function populateEmoji(
host: host ?? IsNull(), host: host ?? IsNull(),
})) || null; })) || null;
const cacheKey = `${name} ${host}`; const emoji = await EmojiCache.fetch(
let emoji = await EmojiCache.fetch(cacheKey, queryOrNull); `${name} ${host}`,
queryOrNull,
if (emoji && !(emoji.width && emoji.height)) { false,
emoji = await queryOrNull(); (cache) => !!cache?.width && !!cache?.height,
await EmojiCache.set(cacheKey, emoji); );
}
if (emoji == null) return null; if (emoji == null) return null;

View file

@ -3,6 +3,7 @@ import type { User } from "@/models/entities/user.js";
import type { Note } from "@/models/entities/note.js"; import type { Note } from "@/models/entities/note.js";
import { Notes, Users } from "@/models/index.js"; import { Notes, Users } from "@/models/index.js";
import { generateVisibilityQuery } from "./generate-visibility-query.js"; import { generateVisibilityQuery } from "./generate-visibility-query.js";
import { prepared, scyllaClient } from "@/db/scylla.js";
/** /**
* Get note for API processing, taking into account visibility. * Get note for API processing, taking into account visibility.
@ -11,13 +12,27 @@ export async function getNote(
noteId: Note["id"], noteId: Note["id"],
me: { id: User["id"] } | null, me: { id: User["id"] } | null,
) { ) {
let note: Note | null = null;
if (scyllaClient) {
const result = await scyllaClient.execute(
prepared.note.select.byId,
[noteId],
{ prepare: true },
);
if (result.rowLength > 0) {
const visibility: string = result.rows[0].get("visibility");
if (!me) {
}
}
}
const query = Notes.createQueryBuilder("note").where("note.id = :id", { const query = Notes.createQueryBuilder("note").where("note.id = :id", {
id: noteId, id: noteId,
}); });
generateVisibilityQuery(query, me); generateVisibilityQuery(query, me);
const note = await query.getOne(); note = await query.getOne();
if (note == null) { if (note == null) {
throw new IdentifiableError( throw new IdentifiableError(

View file

@ -16,6 +16,7 @@ import { verifyLink } from "@/services/fetch-rel-me.js";
import { ApiError } from "../../error.js"; import { ApiError } from "../../error.js";
import config from "@/config/index.js"; import config from "@/config/index.js";
import define from "../../define.js"; import define from "../../define.js";
import { userByIdCache } from "@/services/user-cache.js";
export const meta = { export const meta = {
tags: ["account"], tags: ["account"],
@ -305,7 +306,13 @@ export default define(meta, paramDef, async (ps, _user, token) => {
updateUsertags(user, tags); updateUsertags(user, tags);
//#endregion //#endregion
if (Object.keys(updates).length > 0) await Users.update(user.id, updates); if (Object.keys(updates).length > 0) {
await Users.update(user.id, updates);
await userByIdCache.set(
user.id,
await Users.findOneByOrFail({ id: user.id }),
);
}
if (Object.keys(profileUpdates).length > 0) if (Object.keys(profileUpdates).length > 0)
await UserProfiles.update(user.id, profileUpdates); await UserProfiles.update(user.id, profileUpdates);
@ -319,7 +326,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
publishUserEvent( publishUserEvent(
user.id, user.id,
"updateUserProfile", "updateUserProfile",
await UserProfiles.findOneBy({ userId: user.id }), await UserProfiles.findOneByOrFail({ userId: user.id }),
); );
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認

View file

@ -1,6 +1,6 @@
import { In } from "typeorm"; import { In } from "typeorm";
import create from "@/services/note/create.js"; import create from "@/services/note/create.js";
import type { User } from "@/models/entities/user.js"; import type { CacheableUser, User } from "@/models/entities/user.js";
import { import {
Users, Users,
DriveFiles, DriveFiles,
@ -17,6 +17,7 @@ import { ApiError } from "../../error.js";
import define from "../../define.js"; import define from "../../define.js";
import { HOUR } from "@/const.js"; import { HOUR } from "@/const.js";
import { getNote } from "../../common/getters.js"; import { getNote } from "../../common/getters.js";
import { userByIdCache } from "@/services/user-cache.js";
export const meta = { export const meta = {
tags: ["notes"], tags: ["notes"],
@ -190,15 +191,25 @@ export default define(meta, paramDef, async (ps, user) => {
if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked); if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked);
let visibleUsers: User[] = []; let visibleUsers: User[] = [];
if (ps.visibleUserIds) { if (ps.visibleUserIds) {
visibleUsers = await Users.findBy({ visibleUsers = (
id: In(ps.visibleUserIds), await Promise.all(
}); ps.visibleUserIds.map((id) =>
userByIdCache.fetchMaybe(id, () =>
Users.findOneBy({ id }).then((user) => user ?? undefined),
),
),
)
).filter((user) => user !== undefined) as CacheableUser[];
} }
let files: DriveFile[] = []; let files: DriveFile[] = [];
const fileIds = const fileIds =
ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; ps.fileIds && ps.fileIds.length > 0
if (fileIds != null) { ? ps.fileIds
: ps.mediaIds && ps.mediaIds.length > 0
? ps.mediaIds
: null;
if (fileIds && fileIds.length > 0) {
files = await DriveFiles.createQueryBuilder("file") files = await DriveFiles.createQueryBuilder("file")
.where("file.userId = :userId AND file.id IN (:...fileIds)", { .where("file.userId = :userId AND file.id IN (:...fileIds)", {
userId: user.id, userId: user.id,
@ -210,7 +221,7 @@ export default define(meta, paramDef, async (ps, user) => {
} }
let renote: Note | null = null; let renote: Note | null = null;
if (ps.renoteId != null) { if (ps.renoteId) {
// Fetch renote to note // Fetch renote to note
renote = await getNote(ps.renoteId, user).catch((e) => { renote = await getNote(ps.renoteId, user).catch((e) => {
if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24")

View file

@ -677,9 +677,12 @@ async function renderNoteOrRenoteActivity(data: Option, note: Note) {
function incRenoteCount(renote: Note) { function incRenoteCount(renote: Note) {
if (scyllaClient) { if (scyllaClient) {
const date = new Date(renote.createdAt.getTime());
scyllaClient.execute(prepared.note.update.renoteCount, [ scyllaClient.execute(prepared.note.update.renoteCount, [
renote.renoteCount + 1, renote.renoteCount + 1,
renote.score + 1, renote.score + 1,
date,
date,
renote.id, renote.id,
]); ]);
} else { } else {
@ -768,7 +771,6 @@ async function insertNote(
// 投稿を作成 // 投稿を作成
try { try {
if (scyllaClient) { if (scyllaClient) {
const noteEmojis = await populateEmojis(insert.emojis, user.host);
await scyllaClient.execute( await scyllaClient.execute(
prepared.note.insert, prepared.note.insert,
[ [
@ -788,7 +790,7 @@ async function insertNote(
data.files, data.files,
insert.visibleUserIds, insert.visibleUserIds,
insert.mentions, insert.mentions,
noteEmojis, insert.emojis,
insert.tags, insert.tags,
insert.hasPoll, insert.hasPoll,
insert.threadId, insert.threadId,
@ -800,7 +802,6 @@ async function insertNote(
null, null,
null, null,
null, null,
null,
], ],
{ prepare: true }, { prepare: true },
); );

View file

@ -119,7 +119,7 @@ export default async function (
if (scyllaClient) { if (scyllaClient) {
const date = new Date(note.createdAt.getTime()); const date = new Date(note.createdAt.getTime());
await scyllaClient.execute(prepared.note.delete, [date, date], { await scyllaClient.execute(prepared.note.delete, [date, date, note.id], {
prepare: true, prepare: true,
}); });
} }

View file

@ -22,6 +22,7 @@ import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js
import type { NoteReaction } from "@/models/entities/note-reaction.js"; import type { NoteReaction } from "@/models/entities/note-reaction.js";
import { IdentifiableError } from "@/misc/identifiable-error.js"; import { IdentifiableError } from "@/misc/identifiable-error.js";
import { prepared, scyllaClient } from "@/db/scylla.js"; import { prepared, scyllaClient } from "@/db/scylla.js";
import { populateEmojis } from "@/misc/populate-emojis.js";
export default async ( export default async (
user: { id: User["id"]; host: User["host"] }, user: { id: User["id"]; host: User["host"] },
@ -105,10 +106,18 @@ export default async (
if (scyllaClient) { if (scyllaClient) {
const current = Math.max(note.reactions[_reaction] ?? 0, 0); const current = Math.max(note.reactions[_reaction] ?? 0, 0);
note.reactions[_reaction] = current + 1; note.reactions[_reaction] = current + 1;
const emojiName = decodeReaction(_reaction).reaction.replaceAll(":", "");
const date = new Date(note.createdAt.getTime()); const date = new Date(note.createdAt.getTime());
await scyllaClient.execute( await scyllaClient.execute(
prepared.note.update.reactions, prepared.note.update.reactions,
[note.reactions, (note.score ?? 0) + 1, date, date], [
note.emojis.concat(emojiName),
note.reactions,
(note.score ?? 0) + 1,
date,
date,
note.id,
],
{ prepare: true }, { prepare: true },
); );
} else { } else {

View file

@ -56,9 +56,19 @@ export default async (
note.reactions[reaction.reaction] = count; note.reactions[reaction.reaction] = count;
} }
const date = new Date(note.createdAt.getTime()); const date = new Date(note.createdAt.getTime());
const emojiName = reaction.reaction.replaceAll(":", "");
const emojiIndex = note.emojis.indexOf(emojiName);
if (emojiIndex >= 0 && count === 0) note.emojis.splice(emojiIndex, 1);
await scyllaClient.execute( await scyllaClient.execute(
prepared.note.update.reactions, prepared.note.update.reactions,
[note.reactions, Math.max((note.score ?? 0) - 1, 0), date, date], [
note.emojis,
note.reactions,
Math.max((note.score ?? 0) - 1, 0),
date,
date,
note.id,
],
{ prepare: true }, { prepare: true },
); );
} else { } else {