import config from "@/config/index.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 { Client, types } from "cassandra-driver"; import type { User } from "@/models/entities/user.js"; import { ChannelFollowingsCache, LocalFollowingsCache } from "@/misc/cache.js"; function newClient(): Client | null { if (!config.scylla) { return null; } return new Client({ contactPoints: config.scylla.nodes, localDataCenter: config.scylla.localDataCentre, keyspace: config.scylla.keyspace, }); } export const scyllaClient = newClient(); export const prepared = { note: { insert: `INSERT INTO note ( "createdAtDate", "createdAt", "id", "visibility", "content", "name", "cw", "localOnly", "renoteCount", "repliesCount", "uri", "url", "score", "files", "visibleUserIds", "mentions", "mentionedRemoteUsers", "emojis", "tags", "hasPoll", "threadId", "channelId", "userId", "userHost", "replyId", "replyUserId", "replyUserHost", "renoteId", "renoteUserId", "renoteUserHost", "reactions", "noteEdit", "updatedAt" ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, select: { byDate: `SELECT * FROM note WHERE "createdAtDate" = ?`, byUri: `SELECT * FROM note WHERE "uri" IN ?`, byUrl: `SELECT * FROM note WHERE "url" IN ?`, byId: `SELECT * FROM note_by_id WHERE "id" IN ?`, byUserId: `SELECT * FROM note_by_userid WHERE "userId" IN ?`, }, delete: `DELETE FROM note WHERE "createdAtDate" = ? AND "createdAt" = ? AND "id" = ?`, update: { renoteCount: `UPDATE note SET "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: { insert: `INSERT INTO reaction ("id", "noteId", "userId", "reaction", "emoji", "createdAt") VALUES (?, ?, ?, ?, ?, ?)`, select: { byNoteId: `SELECT * FROM reaction WHERE "noteId" IN ?`, byUserId: `SELECT * FROM reaction_by_userid WHERE "userId" IN ?`, byNoteAndUser: `SELECT * FROM reaction WHERE "noteId" = ? AND "userId" = ?`, byId: `SELECT * FROM reaction WHERE "id" IN ?`, }, delete: `DELETE FROM reaction WHERE "noteId" = ? AND "userId" = ?`, }, }; export interface ScyllaDriveFile { id: string; type: string; createdAt: Date; name: string; comment: string | null; blurhash: string | null; url: string; thumbnailUrl: string; isSensitive: boolean; isLink: boolean; md5: string; size: number; width: number | null; height: number | null; } export interface ScyllaNoteEditHistory { content: string; cw: string; files: ScyllaDriveFile[]; updatedAt: Date; } export type ScyllaNote = Note & { createdAtDate: Date; files: ScyllaDriveFile[]; 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") ?? null, name: row.get("name") ?? null, cw: row.get("cw") ?? null, localOnly: row.get("localOnly"), renoteCount: row.get("renoteCount"), repliesCount: row.get("repliesCount"), uri: row.get("uri") ?? null, url: row.get("url") ?? null, 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") ?? false, threadId: row.get("threadId") ?? null, channelId: row.get("channelId") ?? null, userId: row.get("userId"), userHost: row.get("userHost") ?? null, replyId: row.get("replyId") ?? null, replyUserId: row.get("replyUserId") ?? null, replyUserHost: row.get("replyUserHost") ?? null, renoteId: row.get("renoteId") ?? null, renoteUserId: row.get("renoteUserId") ?? null, renoteUserHost: row.get("renoteUserHost") ?? null, reactions: row.get("reactions") ?? {}, noteEdit: row.get("noteEdit") ?? [], updatedAt: row.get("updatedAt") ?? null, mentionedRemoteUsers: row.get("mentionedRemoteUsers") ?? "[]", /* unused postgres denormalization */ channel: null, renote: null, reply: null, user: null, }; } export interface ScyllaNoteReaction extends NoteReaction { emoji: PopulatedEmoji; } export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction { return { id: row.get("id"), noteId: row.get("noteId"), userId: row.get("userId"), reaction: row.get("reaction"), createdAt: row.get("createdAt"), emoji: row.get("emoji"), }; } export async function isVisible( note: ScyllaNote, user: { id: User["id"] } | null, ): Promise { let visible = false; if ( ["public", "home"].includes(note.visibility) // public post ) { visible = true; } else if (user) { const cache = await LocalFollowingsCache.init(user.id); visible = note.userId === user.id || // my own post note.visibleUserIds.includes(user.id) || // visible to me note.mentions.includes(user.id) || // mentioned me (note.visibility === "followers" && (await cache.isFollowing(note.userId))) || // following note.replyUserId === user.id; // replied to myself } return visible; } export async function filterChannel( notes: ScyllaNote[], user: { id: User["id"] } | null, ): Promise { let foundNotes = notes; if (!user) { foundNotes = foundNotes.filter((note) => !note.channelId); } else { const channelNotes = foundNotes.filter((note) => !!note.channelId); if (channelNotes.length > 0) { const cache = await ChannelFollowingsCache.init(user.id); const followingIds = await cache.getAll(); foundNotes = foundNotes.filter( (note) => !note.channelId || followingIds.includes(note.channelId), ); } } return foundNotes; } export async function filterReply( notes: ScyllaNote[], withReplies: boolean, user: { id: User["id"] } | null, ): Promise { let foundNotes = notes; if (!user) { foundNotes = foundNotes.filter( (note) => !note.replyId || note.replyUserId === note.userId, ); } else if (!withReplies) { foundNotes = foundNotes.filter( (note) => !note.replyId || note.replyUserId === user.id || note.userId === user.id || note.replyUserId === note.userId, ); } return foundNotes; }