feat: Discord-Bild und -Name im Dashboard
Wer ueber Discord hereinkommt, sieht sein Bild in der Kopfzeile, auf der
Kontoseite und neben seinen Eintraegen im Verlauf.
DIE ADRESSE BAUT DER DIENST, nicht die Oberflaeche. Gespeichert wird nur der
Bildstempel; das URL-Schema von Discord steht an EINER Stelle (avatarUrl in
db.ts) und nicht in jeder Ansicht, die ein Bild zeigt. Es hat drei Faelle, und
alle drei stehen dort:
- Stempel faengt mit "a_" an -> bewegtes Bild, muss als .gif angefragt werden
- Stempel vorhanden -> .png mit gewuenschter Groesse
- kein Stempel -> Discords Ersatzbild, dessen Nummer sich aus
der ID ergibt: (id >> 22) % 6. Das ist die
NEUE Rechnung; die alte ueber den
Diskriminator gilt seit den Pseudonymen
nicht mehr. Gegengerechnet an drei IDs.
Bild und Anzeigename werden BEI JEDER ANMELDUNG nachgezogen: wer sein Bild
wechselt, saehe sonst monatelang das alte und fragte sich, ob er im richtigen
Konto sitzt. Der Benutzername im Dashboard bleibt dagegen, wie er ist -- an ihm
haengen die Eintraege im Verlauf.
Der Avatar-Baustein kennt drei Faelle und keinen kaputten: Bild, sonst die
Initialen auf einer Farbe, die sich aus dem Namen ergibt (damit sie beim
Neuladen nicht springt), und fuer einen Token einen Schluessel -- ein Token hat
kein Gesicht, ein Platzhalterbild daneben waere eine Behauptung. Laedt das Bild
nicht (CDN nicht erreichbar, Stempel veraltet), faellt es auf die Initialen
zurueck statt ein kaputtes Bildsymbol zu zeigen.
Nachgemessen: cdn.discordapp.com liefert das Ersatzbild (200, image/png), die
Form der Bildadresse stimmt, und ein Passwort-Konto bekommt sauber avatar:null.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import type { Context } from 'hono'
|
||||
import { deleteCookie, getCookie, setCookie } from 'hono/cookie'
|
||||
import { config } from '../config.js'
|
||||
import {
|
||||
avatarUrl,
|
||||
db,
|
||||
logEvent,
|
||||
now,
|
||||
@@ -198,7 +199,7 @@ dashRoutes.post('/auth/login', async (c) => {
|
||||
expires: new Date(session.expiresAt),
|
||||
})
|
||||
|
||||
return c.json({ user: { id: user.id, username: user.username } })
|
||||
return c.json({ user: zeigen(user) })
|
||||
})
|
||||
|
||||
dashRoutes.post('/auth/logout', (c) => {
|
||||
@@ -208,10 +209,23 @@ dashRoutes.post('/auth/logout', (c) => {
|
||||
return c.json({ ok: true })
|
||||
})
|
||||
|
||||
/** Wer gerade angemeldet ist — samt Bild, wenn es eines gibt.
|
||||
*
|
||||
* Die Adresse wird HIER gebaut und nicht in der Oberflaeche: Discords
|
||||
* URL-Schema (gif bei bewegten Bildern, Ersatzbild aus der ID gerechnet)
|
||||
* gehoert an eine Stelle und nicht in jede Ansicht, die ein Bild zeigt. */
|
||||
const zeigen = (user: User) => ({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
discord: Boolean(user.discord_id),
|
||||
discordName: user.discord_name,
|
||||
avatar: avatarUrl(user.discord_id, user.discord_avatar, 64),
|
||||
})
|
||||
|
||||
dashRoutes.get('/auth/me', (c) => {
|
||||
const user = userForSession(getCookie(c, SESSION_COOKIE))
|
||||
if (!user) return c.json({ error: 'nicht angemeldet' }, 401)
|
||||
return c.json({ user: { id: user.id, username: user.username } })
|
||||
return c.json({ user: zeigen(user) })
|
||||
})
|
||||
|
||||
/** Was die Anmeldeseite wissen muss, BEVOR jemand angemeldet ist.
|
||||
@@ -569,12 +583,32 @@ dashRoutes.get('/events', (c) => {
|
||||
|
||||
// Wer ueberhaupt vorkommt -- damit die Oberflaeche einen Filter anbieten
|
||||
// kann, ohne alle Eintraege durchzublaettern.
|
||||
const akteure = db
|
||||
const roheAkteure = db
|
||||
.prepare(
|
||||
`SELECT actor, actor_kind, COUNT(*) AS n, MAX(at) AS zuletzt
|
||||
FROM events GROUP BY actor, actor_kind ORDER BY n DESC`,
|
||||
`SELECT e.actor, e.actor_kind, COUNT(*) AS n, MAX(e.at) AS zuletzt,
|
||||
u.discord_id, u.discord_avatar
|
||||
FROM events e
|
||||
LEFT JOIN users u ON u.username = e.actor AND e.actor_kind = 'user'
|
||||
GROUP BY e.actor, e.actor_kind ORDER BY n DESC`,
|
||||
)
|
||||
.all() as { actor: string; actor_kind: string; n: number; zuletzt: number }[]
|
||||
.all() as {
|
||||
actor: string
|
||||
actor_kind: string
|
||||
n: number
|
||||
zuletzt: number
|
||||
discord_id: string | null
|
||||
discord_avatar: string | null
|
||||
}[]
|
||||
|
||||
// Das Bild nur fuer Menschen. Ein Token hat kein Gesicht, und ein
|
||||
// Platzhalterbild daneben waere eine Behauptung.
|
||||
const akteure = roheAkteure.map((a) => ({
|
||||
actor: a.actor,
|
||||
actor_kind: a.actor_kind,
|
||||
n: a.n,
|
||||
zuletzt: a.zuletzt,
|
||||
avatar: avatarUrl(a.discord_id, a.discord_avatar, 32),
|
||||
}))
|
||||
|
||||
// Ab wann aufgezeichnet wird. Alles davor steht nur in der Galerie, und
|
||||
// das gehoert dazugesagt statt verschwiegen.
|
||||
|
||||
@@ -126,6 +126,7 @@ discordRoutes.get('/discord/callback', async (c) => {
|
||||
id: string
|
||||
username: string
|
||||
global_name?: string | null
|
||||
avatar?: string | null
|
||||
}
|
||||
|
||||
// -- Und welche Rolle hat er hier -----------------------------------
|
||||
@@ -160,8 +161,9 @@ discordRoutes.get('/discord/callback', async (c) => {
|
||||
| User
|
||||
| undefined
|
||||
|
||||
const anzeige = (me.global_name || me.username || `discord-${me.id}`).slice(0, 40)
|
||||
|
||||
if (!user) {
|
||||
const anzeige = (me.global_name || me.username || `discord-${me.id}`).slice(0, 40)
|
||||
// Der Name muss eindeutig sein; zwei Leute koennen denselben
|
||||
// Anzeigenamen haben. Bei Kollision haengt die Discord-ID dran.
|
||||
const belegt = db.prepare('SELECT 1 FROM users WHERE username = ?').get(anzeige)
|
||||
@@ -172,13 +174,22 @@ discordRoutes.get('/discord/callback', async (c) => {
|
||||
// kommt also ausschliesslich ueber Discord herein.
|
||||
const info = db
|
||||
.prepare(
|
||||
`INSERT INTO users (username, password_hash, created_at, discord_id)
|
||||
VALUES (?, 'nur-ueber-discord', ?, ?)`,
|
||||
`INSERT INTO users (username, password_hash, created_at, discord_id,
|
||||
discord_avatar, discord_name)
|
||||
VALUES (?, 'nur-ueber-discord', ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(username, now(), me.id)
|
||||
.run(username, now(), me.id, me.avatar ?? null, anzeige)
|
||||
|
||||
user = db.prepare('SELECT * FROM users WHERE id = ?').get(info.lastInsertRowid) as User
|
||||
console.log(`[discord] Konto angelegt: ${username} (${me.id})`)
|
||||
} else {
|
||||
// BEI JEDER ANMELDUNG nachziehen. Wer sein Bild wechselt, sieht
|
||||
// sonst monatelang das alte — und wundert sich, ob er im richtigen
|
||||
// Konto sitzt. Der ANZEIGENAME im Dashboard bleibt dagegen, wie er
|
||||
// ist: an ihm haengen die Eintraege im Verlauf.
|
||||
db.prepare(
|
||||
'UPDATE users SET discord_avatar = ?, discord_name = ? WHERE id = ?',
|
||||
).run(me.avatar ?? null, anzeige, user.id)
|
||||
}
|
||||
|
||||
const session = createSession(user.id)
|
||||
|
||||
Reference in New Issue
Block a user