Merge pull request '[PR]: fix: poll result update' (#10322) from nmkj/calckey:fix-poll-update into develop
Reviewed-on: https://codeberg.org/calckey/calckey/pulls/10322
This commit is contained in:
commit
a1e9609605
|
@ -28,7 +28,7 @@ import {
|
||||||
import { db } from "@/db/postgre.js";
|
import { db } from "@/db/postgre.js";
|
||||||
import { IdentifiableError } from "@/misc/identifiable-error.js";
|
import { IdentifiableError } from "@/misc/identifiable-error.js";
|
||||||
|
|
||||||
async function populatePoll(note: Note, meId: User["id"] | null) {
|
export async function populatePoll(note: Note, meId: User["id"] | null) {
|
||||||
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||||
const choices = poll.choices.map((c) => ({
|
const choices = poll.choices.map((c) => ({
|
||||||
text: c,
|
text: c,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type Bull from "bull";
|
import type Bull from "bull";
|
||||||
import { In } from "typeorm";
|
import { Notes, PollVotes } from "@/models/index.js";
|
||||||
import { Notes, Polls, PollVotes } from "@/models/index.js";
|
|
||||||
import { queueLogger } from "../logger.js";
|
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";
|
||||||
|
|
||||||
const logger = queueLogger.createSubLogger("ended-poll-notification");
|
const logger = queueLogger.createSubLogger("ended-poll-notification");
|
||||||
|
|
||||||
|
@ -32,5 +32,8 @@ export async function endedPollNotification(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broadcast the poll result once it ends
|
||||||
|
await deliverQuestionUpdate(note.id);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,10 @@ import type {
|
||||||
import { htmlToMfm } from "../misc/html-to-mfm.js";
|
import { htmlToMfm } from "../misc/html-to-mfm.js";
|
||||||
import { extractApHashtags } from "./tag.js";
|
import { extractApHashtags } from "./tag.js";
|
||||||
import { unique, toArray, toSingle } from "@/prelude/array.js";
|
import { unique, toArray, toSingle } from "@/prelude/array.js";
|
||||||
import { extractPollFromQuestion, updateQuestion } from "./question.js";
|
import { extractPollFromQuestion } from "./question.js";
|
||||||
import vote from "@/services/note/polls/vote.js";
|
import vote from "@/services/note/polls/vote.js";
|
||||||
import { apLogger } from "../logger.js";
|
import { apLogger } from "../logger.js";
|
||||||
import { DriveFile } from "@/models/entities/drive-file.js";
|
import { DriveFile } from "@/models/entities/drive-file.js";
|
||||||
import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
|
||||||
import { extractDbHost, toPuny } from "@/misc/convert-host.js";
|
import { extractDbHost, toPuny } from "@/misc/convert-host.js";
|
||||||
import {
|
import {
|
||||||
Emojis,
|
Emojis,
|
||||||
|
@ -334,9 +333,6 @@ export async function createNote(
|
||||||
`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`,
|
`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`,
|
||||||
);
|
);
|
||||||
await vote(actor, reply, index);
|
await vote(actor, reply, index);
|
||||||
|
|
||||||
// リモートフォロワーにUpdate配信
|
|
||||||
deliverQuestionUpdate(reply.id);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import Resolver from "../resolver.js";
|
import Resolver from "../resolver.js";
|
||||||
import type { IObject, IQuestion } from "../type.js";
|
import type { IObject, IQuestion } from "../type.js";
|
||||||
import { isQuestion } from "../type.js";
|
import { getApId, isQuestion } from "../type.js";
|
||||||
import { apLogger } from "../logger.js";
|
import { apLogger } from "../logger.js";
|
||||||
import { Notes, Polls } from "@/models/index.js";
|
import { Notes, Polls } from "@/models/index.js";
|
||||||
import type { IPoll } from "@/models/entities/poll.js";
|
import type { IPoll } from "@/models/entities/poll.js";
|
||||||
|
@ -47,11 +47,14 @@ export async function extractPollFromQuestion(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update votes of Question
|
* Update votes of Question
|
||||||
* @param uri URI of AP Question object
|
* @param value URI of AP Question object or object itself
|
||||||
* @returns true if updated
|
* @returns true if updated
|
||||||
*/
|
*/
|
||||||
export async function updateQuestion(value: any, resolver?: Resolver) {
|
export async function updateQuestion(
|
||||||
const uri = typeof value === "string" ? value : value.id;
|
value: string | IQuestion,
|
||||||
|
resolver?: Resolver,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const uri = typeof value === "string" ? value : getApId(value);
|
||||||
|
|
||||||
// Skip if URI points to this server
|
// Skip if URI points to this server
|
||||||
if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local");
|
if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local");
|
||||||
|
@ -65,22 +68,23 @@ export async function updateQuestion(value: any, resolver?: Resolver) {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// resolve new Question object
|
// resolve new Question object
|
||||||
if (resolver == null) resolver = new Resolver();
|
const _resolver = resolver ?? new Resolver();
|
||||||
const question = (await resolver.resolve(value)) as IQuestion;
|
const question = (await _resolver.resolve(value)) as IQuestion;
|
||||||
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||||
|
|
||||||
if (question.type !== "Question") throw new Error("object is not a Question");
|
if (question.type !== "Question") throw new Error("object is not a Question");
|
||||||
|
|
||||||
const apChoices = question.oneOf || question.anyOf;
|
const apChoices = question.oneOf || question.anyOf;
|
||||||
|
if (!apChoices) return false;
|
||||||
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
for (const choice of poll.choices) {
|
for (const choice of poll.choices) {
|
||||||
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
||||||
const newCount = apChoices!.filter((ap) => ap.name === choice)[0].replies!
|
const newCount = apChoices.filter((ap) => ap.name === choice)[0].replies
|
||||||
.totalItems;
|
?.totalItems;
|
||||||
|
|
||||||
if (oldCount !== newCount) {
|
if (newCount !== undefined && oldCount !== newCount) {
|
||||||
changed = true;
|
changed = true;
|
||||||
poll.votes[poll.choices.indexOf(choice)] = newCount;
|
poll.votes[poll.choices.indexOf(choice)] = newCount;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import define from "../../define.js";
|
import define from "../../define.js";
|
||||||
import config from "@/config/index.js";
|
|
||||||
import { createPerson } from "@/remote/activitypub/models/person.js";
|
import { createPerson } from "@/remote/activitypub/models/person.js";
|
||||||
import { createNote } from "@/remote/activitypub/models/note.js";
|
import { createNote } from "@/remote/activitypub/models/note.js";
|
||||||
import DbResolver from "@/remote/activitypub/db-resolver.js";
|
import DbResolver from "@/remote/activitypub/db-resolver.js";
|
||||||
|
@ -9,11 +8,13 @@ import { extractDbHost } from "@/misc/convert-host.js";
|
||||||
import { Users, Notes } from "@/models/index.js";
|
import { Users, Notes } from "@/models/index.js";
|
||||||
import type { Note } from "@/models/entities/note.js";
|
import type { Note } from "@/models/entities/note.js";
|
||||||
import type { CacheableLocalUser, User } from "@/models/entities/user.js";
|
import type { CacheableLocalUser, User } from "@/models/entities/user.js";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
|
||||||
import { isActor, isPost, getApId } from "@/remote/activitypub/type.js";
|
import { isActor, isPost, getApId } from "@/remote/activitypub/type.js";
|
||||||
import type { SchemaType } from "@/misc/schema.js";
|
import type { SchemaType } from "@/misc/schema.js";
|
||||||
import { HOUR } from "@/const.js";
|
import { HOUR } from "@/const.js";
|
||||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||||
|
import { updateQuestion } from "@/remote/activitypub/models/question.js";
|
||||||
|
import { populatePoll } from "@/models/repositories/note.js";
|
||||||
|
import { redisClient } from "@/db/redis.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["federation"],
|
tags: ["federation"],
|
||||||
|
@ -104,18 +105,29 @@ async function fetchAny(
|
||||||
|
|
||||||
const dbResolver = new DbResolver();
|
const dbResolver = new DbResolver();
|
||||||
|
|
||||||
let local = await mergePack(
|
const [user, note] = await Promise.all([
|
||||||
me,
|
|
||||||
...(await Promise.all([
|
|
||||||
dbResolver.getUserFromApId(uri),
|
dbResolver.getUserFromApId(uri),
|
||||||
dbResolver.getNoteFromApId(uri),
|
dbResolver.getNoteFromApId(uri),
|
||||||
])),
|
]);
|
||||||
);
|
let local = await mergePack(me, user, note);
|
||||||
if (local != null) return local;
|
if (local) {
|
||||||
|
if (local.type === "Note" && note?.uri && note.hasPoll) {
|
||||||
|
// Update questions if the stored (remote) note contains the poll
|
||||||
|
const key = `pollFetched:${note.uri}`;
|
||||||
|
if ((await redisClient.exists(key)) === 0) {
|
||||||
|
if (await updateQuestion(note.uri)) {
|
||||||
|
local.object.poll = await populatePoll(note, me?.id ?? null);
|
||||||
|
}
|
||||||
|
// Allow fetching the poll again after 1 minute
|
||||||
|
await redisClient.set(key, 1, "EX", 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
|
||||||
// fetching Object once from remote
|
// fetching Object once from remote
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
const object = (await resolver.resolve(uri)) as any;
|
const object = await resolver.resolve(uri);
|
||||||
|
|
||||||
// /@user If a URI other than the id is specified,
|
// /@user If a URI other than the id is specified,
|
||||||
// the URI is determined here
|
// the URI is determined here
|
||||||
|
@ -123,8 +135,8 @@ async function fetchAny(
|
||||||
local = await mergePack(
|
local = await mergePack(
|
||||||
me,
|
me,
|
||||||
...(await Promise.all([
|
...(await Promise.all([
|
||||||
dbResolver.getUserFromApId(object.id),
|
dbResolver.getUserFromApId(getApId(object)),
|
||||||
dbResolver.getNoteFromApId(object.id),
|
dbResolver.getNoteFromApId(getApId(object)),
|
||||||
])),
|
])),
|
||||||
);
|
);
|
||||||
if (local != null) return local;
|
if (local != null) return local;
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { createNotification } from "@/services/create-notification.js";
|
||||||
import { deliver } from "@/queue/index.js";
|
import { deliver } from "@/queue/index.js";
|
||||||
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
||||||
import renderVote from "@/remote/activitypub/renderer/vote.js";
|
import renderVote from "@/remote/activitypub/renderer/vote.js";
|
||||||
import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
|
||||||
import {
|
import {
|
||||||
PollVotes,
|
PollVotes,
|
||||||
NoteWatchings,
|
NoteWatchings,
|
||||||
|
@ -178,7 +177,4 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
pollOwner.inbox,
|
pollOwner.inbox,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// リモートフォロワーにUpdate配信
|
|
||||||
deliverQuestionUpdate(note.id);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,23 +34,25 @@
|
||||||
</ul>
|
</ul>
|
||||||
<p v-if="!readOnly">
|
<p v-if="!readOnly">
|
||||||
<span>{{ i18n.t("_poll.totalVotes", { n: total }) }}</span>
|
<span>{{ i18n.t("_poll.totalVotes", { n: total }) }}</span>
|
||||||
|
<span v-if="!closed && !isVoted">
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a
|
<a @click.stop="showResult = !showResult">{{
|
||||||
v-if="!closed && !isVoted"
|
|
||||||
@click.stop="showResult = !showResult"
|
|
||||||
>{{
|
|
||||||
showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult
|
showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult
|
||||||
}}</a
|
}}</a>
|
||||||
>
|
</span>
|
||||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
<span v-if="!isLocal">
|
||||||
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
|
<span> · </span>
|
||||||
|
<a @click.stop="refresh">{{ i18n.ts.reload }}</a>
|
||||||
|
</span>
|
||||||
|
<span v-if="isVoted"> · {{ i18n.ts._poll.voted }}</span>
|
||||||
|
<span v-else-if="closed"> · {{ i18n.ts._poll.closed }}</span>
|
||||||
<span v-if="remaining > 0"> · {{ timer }}</span>
|
<span v-if="remaining > 0"> · {{ timer }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onUnmounted, ref, toRef } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import * as misskey from "calckey-js";
|
import * as misskey from "calckey-js";
|
||||||
import { sum } from "@/scripts/array";
|
import { sum } from "@/scripts/array";
|
||||||
import { pleaseLogin } from "@/scripts/please-login";
|
import { pleaseLogin } from "@/scripts/please-login";
|
||||||
|
@ -67,6 +69,7 @@ const remaining = ref(-1);
|
||||||
|
|
||||||
const total = computed(() => sum(props.note.poll.choices.map((x) => x.votes)));
|
const total = computed(() => sum(props.note.poll.choices.map((x) => x.votes)));
|
||||||
const closed = computed(() => remaining.value === 0);
|
const closed = computed(() => remaining.value === 0);
|
||||||
|
const isLocal = computed(() => !props.note.uri);
|
||||||
const isVoted = computed(
|
const isVoted = computed(
|
||||||
() =>
|
() =>
|
||||||
!props.note.poll.multiple &&
|
!props.note.poll.multiple &&
|
||||||
|
@ -112,6 +115,14 @@ if (props.note.poll.expiresAt) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
if (!props.note.uri) return;
|
||||||
|
const obj = await os.apiWithDialog("ap/show", { uri: props.note.uri });
|
||||||
|
if (obj.type === "Note" && obj.object.poll) {
|
||||||
|
props.note.poll = obj.object.poll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const vote = async (id) => {
|
const vote = async (id) => {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue