wip: resolvers
This commit is contained in:
parent
5cb04189ac
commit
c9aa7646d2
9 changed files with 163 additions and 23 deletions
|
@ -244,6 +244,7 @@ CREATE MATERIALIZED VIEW reaction_by_id AS
|
||||||
CREATE TABLE poll_vote (
|
CREATE TABLE poll_vote (
|
||||||
"noteId" ascii,
|
"noteId" ascii,
|
||||||
"userId" ascii,
|
"userId" ascii,
|
||||||
|
"userHost" text,
|
||||||
"choice" set<int>,
|
"choice" set<int>,
|
||||||
"createdAt" timestamp,
|
"createdAt" timestamp,
|
||||||
PRIMARY KEY ("noteId", "userId")
|
PRIMARY KEY ("noteId", "userId")
|
||||||
|
|
|
@ -160,7 +160,7 @@ export const scyllaQueries = {
|
||||||
},
|
},
|
||||||
poll: {
|
poll: {
|
||||||
select: `SELECT * FROM poll_vote WHERE "noteId" = ?`,
|
select: `SELECT * FROM poll_vote WHERE "noteId" = ?`,
|
||||||
insert: `INSERT INTO poll_vote ("noteId", "userId", "choice", "createdAt") VALUES (?, ?, ?, ?)`,
|
insert: `INSERT INTO poll_vote ("noteId", "userId", "userHost", "choice", "createdAt") VALUES (?, ?, ?, ?, ?)`,
|
||||||
},
|
},
|
||||||
notification: {
|
notification: {
|
||||||
insert: `INSERT INTO notification
|
insert: `INSERT INTO notification
|
||||||
|
|
|
@ -155,6 +155,7 @@ export interface ScyllaPoll {
|
||||||
export interface ScyllaPollVote {
|
export interface ScyllaPollVote {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
userHost: string | null;
|
||||||
choice: Set<number>;
|
choice: Set<number>;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
@ -163,6 +164,7 @@ export function parseScyllaPollVote(row: types.Row): ScyllaPollVote {
|
||||||
return {
|
return {
|
||||||
noteId: row.get("noteId"),
|
noteId: row.get("noteId"),
|
||||||
userId: row.get("userId"),
|
userId: row.get("userId"),
|
||||||
|
userHost: row.get("userHost") ?? null,
|
||||||
choice: new Set(row.get("choice") ?? []),
|
choice: new Set(row.get("choice") ?? []),
|
||||||
createdAt: row.get("createdAt"),
|
createdAt: row.get("createdAt"),
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,14 @@ import { queueLogger } from "../logger.js";
|
||||||
import type { EndedPollNotificationJobData } from "@/queue/types.js";
|
import type { EndedPollNotificationJobData } from "@/queue/types.js";
|
||||||
import { createNotification } from "@/services/create-notification.js";
|
import { createNotification } from "@/services/create-notification.js";
|
||||||
import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
||||||
|
import {
|
||||||
|
parseScyllaNote,
|
||||||
|
parseScyllaPollVote,
|
||||||
|
prepared,
|
||||||
|
scyllaClient,
|
||||||
|
type ScyllaNote,
|
||||||
|
} from "@/db/scylla.js";
|
||||||
|
import type { Note } from "@/models/entities/note.js";
|
||||||
|
|
||||||
const logger = queueLogger.createSubLogger("ended-poll-notification");
|
const logger = queueLogger.createSubLogger("ended-poll-notification");
|
||||||
|
|
||||||
|
@ -11,22 +19,45 @@ export async function endedPollNotification(
|
||||||
job: Bull.Job<EndedPollNotificationJobData>,
|
job: Bull.Job<EndedPollNotificationJobData>,
|
||||||
done: any,
|
done: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const note = await Notes.findOneBy({ id: job.data.noteId });
|
let note: Note | ScyllaNote | null = null;
|
||||||
if (note == null || !note.hasPoll) {
|
if (scyllaClient) {
|
||||||
|
const result = await scyllaClient.execute(
|
||||||
|
prepared.note.select.byId,
|
||||||
|
[job.data.noteId],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
if (result.rowLength > 0) {
|
||||||
|
note = parseScyllaNote(result.first());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
note = await Notes.findOneBy({ id: job.data.noteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!note?.hasPoll) {
|
||||||
done();
|
done();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const votes = await PollVotes.createQueryBuilder("vote")
|
const userIds = [note.userId];
|
||||||
.select("vote.userId")
|
|
||||||
.where("vote.noteId = :noteId", { noteId: note.id })
|
|
||||||
.innerJoinAndSelect("vote.user", "user")
|
|
||||||
.andWhere("user.host IS NULL")
|
|
||||||
.getMany();
|
|
||||||
|
|
||||||
const userIds = [...new Set([note.userId, ...votes.map((v) => v.userId)])];
|
if (scyllaClient) {
|
||||||
|
const votes = await scyllaClient
|
||||||
|
.execute(prepared.poll.select, [note.id], { prepare: true })
|
||||||
|
.then((result) => result.rows.map(parseScyllaPollVote));
|
||||||
|
const localVotes = votes.filter((vote) => !vote.userHost);
|
||||||
|
userIds.push(...localVotes.map(({ userId }) => userId));
|
||||||
|
} else {
|
||||||
|
const votes = await PollVotes.createQueryBuilder("vote")
|
||||||
|
.select("vote.userId")
|
||||||
|
.where("vote.noteId = :noteId", { noteId: note.id })
|
||||||
|
.innerJoinAndSelect("vote.user", "user")
|
||||||
|
.andWhere("user.host IS NULL")
|
||||||
|
.getMany();
|
||||||
|
|
||||||
for (const userId of userIds) {
|
userIds.push(...votes.map((v) => v.userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const userId of new Set(userIds)) {
|
||||||
createNotification(userId, "pollEnded", {
|
createNotification(userId, "pollEnded", {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { uriPersonCache, userByIdCache } from "@/services/user-cache.js";
|
||||||
import type { IObject } from "./type.js";
|
import type { IObject } from "./type.js";
|
||||||
import { getApId } from "./type.js";
|
import { getApId } from "./type.js";
|
||||||
import { resolvePerson } from "./models/person.js";
|
import { resolvePerson } from "./models/person.js";
|
||||||
|
import { parseScyllaNote, prepared, scyllaClient } from "@/db/scylla.js";
|
||||||
|
|
||||||
const publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30);
|
const publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30);
|
||||||
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(
|
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(
|
||||||
|
@ -78,10 +79,40 @@ export default class DbResolver {
|
||||||
if (parsed.local) {
|
if (parsed.local) {
|
||||||
if (parsed.type !== "notes") return null;
|
if (parsed.type !== "notes") return null;
|
||||||
|
|
||||||
|
if (scyllaClient) {
|
||||||
|
const result = await scyllaClient.execute(
|
||||||
|
prepared.note.select.byId,
|
||||||
|
[parsed.id],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
if (result.rowLength > 0) {
|
||||||
|
return parseScyllaNote(result.first());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return await Notes.findOneBy({
|
return await Notes.findOneBy({
|
||||||
id: parsed.id,
|
id: parsed.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (scyllaClient) {
|
||||||
|
let result = await scyllaClient.execute(
|
||||||
|
prepared.note.select.byUri,
|
||||||
|
[parsed.uri],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
if (result.rowLength === 0) {
|
||||||
|
result = await scyllaClient.execute(
|
||||||
|
prepared.note.select.byUrl,
|
||||||
|
[parsed.uri],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (result.rowLength > 0) {
|
||||||
|
return parseScyllaNote(result.first());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return await Notes.findOne({
|
return await Notes.findOne({
|
||||||
where: [
|
where: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,7 +6,10 @@ import { Emojis } from "@/models/index.js";
|
||||||
import renderEmoji from "./emoji.js";
|
import renderEmoji from "./emoji.js";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||||
|
|
||||||
export const renderLike = async (noteReaction: NoteReaction, note: Note) => {
|
export const renderLike = async (
|
||||||
|
noteReaction: NoteReaction,
|
||||||
|
note: { uri: string | null },
|
||||||
|
) => {
|
||||||
const reaction = noteReaction.reaction;
|
const reaction = noteReaction.reaction;
|
||||||
const meta = await fetchMeta();
|
const meta = await fetchMeta();
|
||||||
|
|
||||||
|
|
|
@ -2,25 +2,49 @@ import config from "@/config/index.js";
|
||||||
import type { User } from "@/models/entities/user.js";
|
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 type { Poll } from "@/models/entities/poll.js";
|
import type { Poll } from "@/models/entities/poll.js";
|
||||||
|
import { type ScyllaPoll, parseScyllaPollVote, prepared, scyllaClient } from "@/db/scylla.js";
|
||||||
|
|
||||||
export default async function renderQuestion(
|
export default async function renderQuestion(
|
||||||
user: { id: User["id"] },
|
user: { id: User["id"] },
|
||||||
note: Note,
|
note: Note,
|
||||||
poll: Poll,
|
poll: Poll | ScyllaPoll,
|
||||||
) {
|
) {
|
||||||
|
let choices: {
|
||||||
|
type: "Note";
|
||||||
|
name: string;
|
||||||
|
replies: { type: "Collection"; totalItems: number };
|
||||||
|
}[] = [];
|
||||||
|
if (scyllaClient) {
|
||||||
|
const votes = await scyllaClient
|
||||||
|
.execute(prepared.poll.select, [note.id], { prepare: true })
|
||||||
|
.then((result) => result.rows.map(parseScyllaPollVote));
|
||||||
|
choices = Object.entries((poll as ScyllaPoll).choices).map(
|
||||||
|
([index, text]) => ({
|
||||||
|
type: "Note",
|
||||||
|
name: text,
|
||||||
|
replies: {
|
||||||
|
type: "Collection",
|
||||||
|
totalItems: votes.filter((vote) => vote.choice.has(parseInt(index)))
|
||||||
|
.length,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
choices = (poll as Poll).choices.map((text, i) => ({
|
||||||
|
type: "Note",
|
||||||
|
name: text,
|
||||||
|
replies: {
|
||||||
|
type: "Collection",
|
||||||
|
totalItems: (poll as Poll).votes[i],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
const question = {
|
const question = {
|
||||||
type: "Question",
|
type: "Question",
|
||||||
id: `${config.url}/questions/${note.id}`,
|
id: `${config.url}/questions/${note.id}`,
|
||||||
actor: `${config.url}/users/${user.id}`,
|
actor: `${config.url}/users/${user.id}`,
|
||||||
content: note.text || "",
|
content: note.text || "",
|
||||||
[poll.multiple ? "anyOf" : "oneOf"]: poll.choices.map((text, i) => ({
|
[poll.multiple ? "anyOf" : "oneOf"]: choices,
|
||||||
name: text,
|
|
||||||
_misskey_votes: poll.votes[i],
|
|
||||||
replies: {
|
|
||||||
type: "Collection",
|
|
||||||
totalItems: poll.votes[i],
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return question;
|
return question;
|
||||||
|
|
|
@ -24,6 +24,12 @@ import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
||||||
import renderFollow from "@/remote/activitypub/renderer/follow.js";
|
import renderFollow from "@/remote/activitypub/renderer/follow.js";
|
||||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||||
import { apLogger } from "@/remote/activitypub/logger.js";
|
import { apLogger } from "@/remote/activitypub/logger.js";
|
||||||
|
import {
|
||||||
|
parseScyllaNote,
|
||||||
|
parseScyllaReaction,
|
||||||
|
prepared,
|
||||||
|
scyllaClient,
|
||||||
|
} from "@/db/scylla.js";
|
||||||
|
|
||||||
export default class Resolver {
|
export default class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
|
@ -146,10 +152,26 @@ export default class Resolver {
|
||||||
|
|
||||||
switch (parsed.type) {
|
switch (parsed.type) {
|
||||||
case "notes":
|
case "notes":
|
||||||
|
if (scyllaClient) {
|
||||||
|
return scyllaClient
|
||||||
|
.execute(prepared.note.select.byId, [parsed.id], { prepare: true })
|
||||||
|
.then((result) => {
|
||||||
|
if (result.rowLength > 0) {
|
||||||
|
const note = parseScyllaNote(result.first());
|
||||||
|
if (parsed.rest === "activity") {
|
||||||
|
// this refers to the create activity and not the note itself
|
||||||
|
return renderActivity(renderCreate(renderNote(note), note));
|
||||||
|
} else {
|
||||||
|
return renderNote(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Note not found");
|
||||||
|
});
|
||||||
|
}
|
||||||
return Notes.findOneByOrFail({ id: parsed.id }).then((note) => {
|
return Notes.findOneByOrFail({ id: parsed.id }).then((note) => {
|
||||||
if (parsed.rest === "activity") {
|
if (parsed.rest === "activity") {
|
||||||
// this refers to the create activity and not the note itself
|
// this refers to the create activity and not the note itself
|
||||||
return renderActivity(renderCreate(renderNote(note)));
|
return renderActivity(renderCreate(renderNote(note), note));
|
||||||
} else {
|
} else {
|
||||||
return renderNote(note);
|
return renderNote(note);
|
||||||
}
|
}
|
||||||
|
@ -159,6 +181,19 @@ export default class Resolver {
|
||||||
renderPerson(user as ILocalUser),
|
renderPerson(user as ILocalUser),
|
||||||
);
|
);
|
||||||
case "questions":
|
case "questions":
|
||||||
|
if (scyllaClient) {
|
||||||
|
return scyllaClient
|
||||||
|
.execute(prepared.note.select.byId, [parsed.id], { prepare: true })
|
||||||
|
.then((result) => {
|
||||||
|
if (result.rowLength > 0) {
|
||||||
|
const note = parseScyllaNote(result.first());
|
||||||
|
if (note.hasPoll && note.poll) {
|
||||||
|
return renderQuestion({ id: note.userId }, note, note.poll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Question not found");
|
||||||
|
});
|
||||||
|
}
|
||||||
// Polls are indexed by the note they are attached to.
|
// Polls are indexed by the note they are attached to.
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
Notes.findOneByOrFail({ id: parsed.id }),
|
Notes.findOneByOrFail({ id: parsed.id }),
|
||||||
|
@ -167,6 +202,19 @@ export default class Resolver {
|
||||||
renderQuestion({ id: note.userId }, note, poll),
|
renderQuestion({ id: note.userId }, note, poll),
|
||||||
);
|
);
|
||||||
case "likes":
|
case "likes":
|
||||||
|
if (scyllaClient) {
|
||||||
|
return scyllaClient
|
||||||
|
.execute(prepared.reaction.select.byId, [parsed.id], {
|
||||||
|
prepare: true,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (result.rowLength > 0) {
|
||||||
|
const reaction = parseScyllaReaction(result.first());
|
||||||
|
return renderActivity(renderLike(reaction, { uri: null }));
|
||||||
|
}
|
||||||
|
throw new Error("Reaction not found");
|
||||||
|
});
|
||||||
|
}
|
||||||
return NoteReactions.findOneByOrFail({ id: parsed.id }).then(
|
return NoteReactions.findOneByOrFail({ id: parsed.id }).then(
|
||||||
(reaction) => renderActivity(renderLike(reaction, { uri: null })),
|
(reaction) => renderActivity(renderLike(reaction, { uri: null })),
|
||||||
);
|
);
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default async function (
|
||||||
|
|
||||||
await scyllaClient.execute(
|
await scyllaClient.execute(
|
||||||
prepared.poll.insert,
|
prepared.poll.insert,
|
||||||
[scyllaNote.id, user.id, Array.from(newChoice), new Date()],
|
[scyllaNote.id, user.id, user.host, Array.from(newChoice), new Date()],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue