feat: ✨ verify links with rel=me
Creates background job to re-check every local link once a week
This commit is contained in:
parent
97a0127dbf
commit
28e271ba77
10 changed files with 121 additions and 3 deletions
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
## Work in progress
|
## Work in progress
|
||||||
|
|
||||||
- Link verification
|
|
||||||
- Better Messaging UI
|
- Better Messaging UI
|
||||||
- Better API Documentation
|
- Better API Documentation
|
||||||
- Remote follow button
|
- Remote follow button
|
||||||
|
@ -118,6 +117,7 @@
|
||||||
- Non-mangled unicode emojis
|
- Non-mangled unicode emojis
|
||||||
- Skin tone selection support
|
- Skin tone selection support
|
||||||
- [DragonflyDB](https://dragonflydb.io/) support as a Redis alternative
|
- [DragonflyDB](https://dragonflydb.io/) support as a Redis alternative
|
||||||
|
- Link verification
|
||||||
|
|
||||||
## Implemented (remote)
|
## Implemented (remote)
|
||||||
|
|
||||||
|
|
|
@ -1124,6 +1124,7 @@ remindMeLater: "Maybe later"
|
||||||
removeQuote: "Remove quote"
|
removeQuote: "Remove quote"
|
||||||
removeRecipient: "Remove recipient"
|
removeRecipient: "Remove recipient"
|
||||||
removeMember: "Remove member"
|
removeMember: "Remove member"
|
||||||
|
verifiedLink: "Verified link"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Reduces the effort of server moderation through automatically recognizing
|
description: "Reduces the effort of server moderation through automatically recognizing
|
||||||
|
|
|
@ -51,6 +51,8 @@ export class UserProfile {
|
||||||
public fields: {
|
public fields: {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
verified?: boolean;
|
||||||
|
lastVerified?: Date;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
@Column("varchar", {
|
@Column("varchar", {
|
||||||
|
|
|
@ -576,6 +576,16 @@ export default function () {
|
||||||
{ removeOnComplete: true, removeOnFail: true },
|
{ removeOnComplete: true, removeOnFail: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
systemQueue.add(
|
||||||
|
"verifyLinks",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
repeat: { cron: "0 0 * * 0" },
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
processSystemQueue(systemQueue);
|
processSystemQueue(systemQueue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import indexAllNotes from "./index-all-notes.js";
|
||||||
|
|
||||||
const jobs = {
|
const jobs = {
|
||||||
indexAllNotes,
|
indexAllNotes,
|
||||||
|
|
||||||
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>>>;
|
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>>>;
|
||||||
|
|
||||||
export default function (q: Bull.Queue) {
|
export default function (q: Bull.Queue) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { cleanCharts } from "./clean-charts.js";
|
||||||
import { checkExpiredMutings } from "./check-expired-mutings.js";
|
import { checkExpiredMutings } from "./check-expired-mutings.js";
|
||||||
import { clean } from "./clean.js";
|
import { clean } from "./clean.js";
|
||||||
import { setLocalEmojiSizes } from "./local-emoji-size.js";
|
import { setLocalEmojiSizes } from "./local-emoji-size.js";
|
||||||
|
import { verifyLinks } from "./verify-links.js";
|
||||||
|
|
||||||
const jobs = {
|
const jobs = {
|
||||||
tickCharts,
|
tickCharts,
|
||||||
|
@ -13,6 +14,7 @@ const jobs = {
|
||||||
checkExpiredMutings,
|
checkExpiredMutings,
|
||||||
clean,
|
clean,
|
||||||
setLocalEmojiSizes,
|
setLocalEmojiSizes,
|
||||||
|
verifyLinks,
|
||||||
} as Record<
|
} as Record<
|
||||||
string,
|
string,
|
||||||
| Bull.ProcessCallbackFunction<Record<string, unknown>>
|
| Bull.ProcessCallbackFunction<Record<string, unknown>>
|
||||||
|
|
62
packages/backend/src/queue/processors/system/verify-links.ts
Normal file
62
packages/backend/src/queue/processors/system/verify-links.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
|
||||||
|
import { UserProfiles } from "@/models/index.js";
|
||||||
|
import { Not } from "typeorm";
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import { getRelMeLinks } from "@/services/fetch-rel-me.js";
|
||||||
|
import config from "@/config/index.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("verify-links");
|
||||||
|
|
||||||
|
export async function verifyLinks(
|
||||||
|
job: Bull.Job<Record<string, unknown>>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info("Verifying links...");
|
||||||
|
|
||||||
|
const usersToVerify = await UserProfiles.findBy({
|
||||||
|
fields: Not(null),
|
||||||
|
userHost: "",
|
||||||
|
});
|
||||||
|
for (const user of usersToVerify) {
|
||||||
|
const fields = user.fields
|
||||||
|
.filter(
|
||||||
|
(x) =>
|
||||||
|
typeof x.name === "string" &&
|
||||||
|
x.name !== "" &&
|
||||||
|
typeof x.value === "string" &&
|
||||||
|
x.value !== "" &&
|
||||||
|
((x.lastVerified &&
|
||||||
|
x.lastVerified.getTime() < Date.now() - 1000 * 60 * 60 * 24 * 14) ||
|
||||||
|
!x.lastVerified),
|
||||||
|
)
|
||||||
|
.map(async (x) => {
|
||||||
|
const relMeLinks = await getRelMeLinks(x.value);
|
||||||
|
const verified = relMeLinks.some((link) =>
|
||||||
|
link.includes(`${config.host}/@${user.user?.host}`),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
name: x.name,
|
||||||
|
value: x.value,
|
||||||
|
verified: verified,
|
||||||
|
lastVerified: new Date(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (fields.length > 0) {
|
||||||
|
const fieldsFinal = await Promise.all(fields);
|
||||||
|
try {
|
||||||
|
await UserProfiles.update(user.userId, {
|
||||||
|
fields: fieldsFinal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
logger.error(`Failed to update user ${user.userId} ${e}`);
|
||||||
|
done(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ("All links successfully verified.");
|
||||||
|
done();
|
||||||
|
}
|
|
@ -12,7 +12,9 @@ import type { UserProfile } from "@/models/entities/user-profile.js";
|
||||||
import { notificationTypes } from "@/types.js";
|
import { notificationTypes } from "@/types.js";
|
||||||
import { normalizeForSearch } from "@/misc/normalize-for-search.js";
|
import { normalizeForSearch } from "@/misc/normalize-for-search.js";
|
||||||
import { langmap } from "@/misc/langmap.js";
|
import { langmap } from "@/misc/langmap.js";
|
||||||
|
import { getRelMeLinks } from "@/services/fetch-rel-me.js";
|
||||||
import { ApiError } from "../../error.js";
|
import { ApiError } from "../../error.js";
|
||||||
|
import config from "@/config/index.js";
|
||||||
import define from "../../define.js";
|
import define from "../../define.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -242,8 +244,17 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
||||||
typeof x.value === "string" &&
|
typeof x.value === "string" &&
|
||||||
x.value !== "",
|
x.value !== "",
|
||||||
)
|
)
|
||||||
.map((x) => {
|
.map(async (x) => {
|
||||||
return { name: x.name, value: x.value };
|
const relMeLinks = await getRelMeLinks(x.value);
|
||||||
|
const verified = relMeLinks.some((link) =>
|
||||||
|
link.includes(`${config.host}/@${user.username}`),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
name: x.name,
|
||||||
|
value: x.value,
|
||||||
|
verified: verified,
|
||||||
|
lastVerified: new Date(),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
packages/backend/src/services/fetch-rel-me.ts
Normal file
16
packages/backend/src/services/fetch-rel-me.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { getHtml } from "@/misc/fetch.js";
|
||||||
|
import { JSDOM } from "jsdom";
|
||||||
|
|
||||||
|
export async function getRelMeLinks(url: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const html = await getHtml(url);
|
||||||
|
const dom = new JSDOM(html);
|
||||||
|
const relMeLinks = [
|
||||||
|
...dom.window.document.querySelectorAll("a[rel='me']"),
|
||||||
|
].map((a) => (a as HTMLAnchorElement).href);
|
||||||
|
return relMeLinks;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -288,10 +288,17 @@
|
||||||
<div v-if="user.fields.length > 0" class="fields">
|
<div v-if="user.fields.length > 0" class="fields">
|
||||||
<dl
|
<dl
|
||||||
v-for="(field, i) in user.fields"
|
v-for="(field, i) in user.fields"
|
||||||
|
:class="field.verified ?? 'verified'"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="field"
|
class="field"
|
||||||
>
|
>
|
||||||
<dt class="name">
|
<dt class="name">
|
||||||
|
<i
|
||||||
|
class="ph-bold ph-seal-check ph-lg ph-fw"
|
||||||
|
style="padding: 5px"
|
||||||
|
:v-tooltip="i18n.ts.verifiedLink"
|
||||||
|
:aria-label="i18n.t('verifiedLink')"
|
||||||
|
></i>
|
||||||
<Mfm
|
<Mfm
|
||||||
:text="field.name"
|
:text="field.name"
|
||||||
:plain="true"
|
:plain="true"
|
||||||
|
@ -744,6 +751,12 @@ onUnmounted(() => {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
> .verified {
|
||||||
|
background-color: var(--hover);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--badge) !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue