v4: a handle with this instance's own host names the local user — another instance writes bob from here as bob@gerbeur.example, so a mention or a link copied from there named no one here: /@bob@gerbeur.example was a 404, and the mention notified nobody. localHandle (shared/validation.ts) drops this instance's host, its API's or its pages', from a handle; resolveUsername reads names by it, so the page, the API and the Open Graph 301 reach bob at /@bob, and so do the mention notifications and the chat's highlight and chime.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,8 @@ import {
|
||||
import { type Page, pageOf } from "../lib/pagination.ts";
|
||||
import { sendToUser } from "./ws-service.ts";
|
||||
import { mentionedUsernames } from "../../shared/mention.ts";
|
||||
import { localHandle } from "../../shared/validation.ts";
|
||||
import { isSelfHost } from "../lib/self-url.ts";
|
||||
|
||||
/** Each id of a user a notification names, beside the field of their name. */
|
||||
const USER_FIELDS = [
|
||||
@@ -359,7 +361,7 @@ export function notifyMentions(
|
||||
for (const username of usernames) {
|
||||
const mentionedRow = db.prepare(
|
||||
`SELECT id FROM users WHERE lower(username) = ?;`,
|
||||
).get(username) as { id: string } | undefined;
|
||||
).get(localHandle(username, isSelfHost)) as { id: string } | undefined;
|
||||
if (!mentionedRow || mentionedRow.id === mentionerUserId) continue;
|
||||
if (skipUserId?.(mentionedRow.id)) continue;
|
||||
|
||||
|
||||
@@ -39,7 +39,12 @@ import { LIKE, likePattern } from "../lib/like.ts";
|
||||
import { parseRows } from "../model/rows.ts";
|
||||
import { isEmailEnabled, sendEmail } from "./email-service.ts";
|
||||
import { renderWelcomeEmail } from "../mail/welcome.ts";
|
||||
import { USERNAME_RE, VALIDATION } from "../../shared/validation.ts";
|
||||
import {
|
||||
localHandle,
|
||||
USERNAME_RE,
|
||||
VALIDATION,
|
||||
} from "../../shared/validation.ts";
|
||||
import { isSelfHost } from "../lib/self-url.ts";
|
||||
import { EVERYONE } from "../../shared/mention.ts";
|
||||
|
||||
/** Selects full `UserRow`s from `users u`, with the inviter's name joined. */
|
||||
@@ -235,17 +240,19 @@ export function getUserByUsername(username: string): User {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user who is or was named `username`, in any case: a page or an
|
||||
* API path naming a user by a former name reaches them, and the caller sends
|
||||
* the client on to the current one.
|
||||
* Returns the user who is or was named `username`, in any case, or by the
|
||||
* handle another instance gives them (`localHandle`): a page or an API path
|
||||
* naming a user so reaches them, and the caller sends the client on to their
|
||||
* current name.
|
||||
*
|
||||
* @throws {APIException} 404 when nobody is or was.
|
||||
*/
|
||||
export function resolveUsername(username: string): User {
|
||||
const name = localHandle(username, isSelfHost);
|
||||
const alias = db.prepare(
|
||||
`SELECT user_id FROM user_aliases WHERE lower(username) = lower(?);`,
|
||||
).get(username) as { user_id: string } | undefined;
|
||||
return alias ? getUserById(alias.user_id) : getUserByUsername(username);
|
||||
).get(name) as { user_id: string } | undefined;
|
||||
return alias ? getUserById(alias.user_id) : getUserByUsername(name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -145,6 +145,7 @@ test("ogMiddleware: a former slug, a former name or another case gets a 301", as
|
||||
[`/@OLD_NAME/dumps/now`, dump.path],
|
||||
[`/@old_name/follow/playlists`, `/@${user.username}/follow/playlists`],
|
||||
[`/@${user.username.toUpperCase()}`, `/@${user.username}`],
|
||||
[`/@${user.username}@gerbeur.test/upvoted`, `/@${user.username}/upvoted`],
|
||||
]
|
||||
) {
|
||||
const { res } = await handle(ogApp(), from);
|
||||
|
||||
@@ -59,6 +59,8 @@ test("public profile: by @name in any case or id, without private fields", async
|
||||
`/api/users/@${user.username}`,
|
||||
`/api/users/@${user.username.toUpperCase()}`,
|
||||
`/api/users/${user.id}`,
|
||||
// As another instance writes them.
|
||||
`/api/users/@${user.username}@gerbeur.test`,
|
||||
]
|
||||
) {
|
||||
const res = await get<PublicUser>(path);
|
||||
@@ -68,6 +70,10 @@ test("public profile: by @name in any case or id, without private fields", async
|
||||
assertFalse("passwordHash" in res.data);
|
||||
}
|
||||
assertEquals((await get(`/api/users/@no_one_here`)).status, 404);
|
||||
assertEquals(
|
||||
(await get(`/api/users/@${user.username}@elsewhere.test`)).status,
|
||||
404,
|
||||
);
|
||||
// Without the `@`, the key is an id.
|
||||
assertEquals((await get(`/api/users/${user.username}`)).status, 404);
|
||||
const me = await get<SelfUser>("/api/me", { auth: user.auth });
|
||||
|
||||
@@ -15,6 +15,7 @@ test("mentions: notify the named, once per context, never the author or an email
|
||||
const named = await createUser();
|
||||
const { id: dumpId } = insertDump({ userId: author.id, title: "Topic" });
|
||||
const body = `hi @${named.username.toUpperCase()} and @${named.username}, ` +
|
||||
`@${named.username}@gerbeur.test as another instance writes them, ` +
|
||||
`me @${author.username}, mail x@${named.username}.com, ` +
|
||||
`[@${named.username}](link) @nobody_known`;
|
||||
const c = await post<Comment>(`/api/dumps/${dumpId}/comments`, {
|
||||
|
||||
Reference in New Issue
Block a user