web/util: move identifier validation functions to separate file

This commit is contained in:
Tulir Asokan 2024-10-14 23:24:17 +03:00
parent c4c5563f9a
commit d77534c1de
3 changed files with 61 additions and 19 deletions

View file

@ -13,20 +13,15 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { parseMXC } from "@/util/validation.ts"
import { UserID } from "./types" import { UserID } from "./types"
const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/
export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => { export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => {
if (!mxc) { const [server, mediaID] = parseMXC(mxc)
if (!mediaID) {
return undefined return undefined
} }
const match = mxc.match(mediaRegex) return `_gomuks/media/${server}/${mediaID}?encrypted=${encrypted}`
if (!match) {
return undefined
}
return `_gomuks/media/${match[1]}/${match[2]}?encrypted=${encrypted}`
} }
export const getEncryptedMediaURL = (mxc?: string): string | undefined => { export const getEncryptedMediaURL = (mxc?: string): string | undefined => {
@ -34,15 +29,11 @@ export const getEncryptedMediaURL = (mxc?: string): string | undefined => {
} }
export const getAvatarURL = (_userID: UserID, mxc?: string): string | undefined => { export const getAvatarURL = (_userID: UserID, mxc?: string): string | undefined => {
if (!mxc) { const [server, mediaID] = parseMXC(mxc)
if (!mediaID) {
return undefined return undefined
// return `_gomuks/avatar/${encodeURIComponent(userID)}` // return `_gomuks/avatar/${encodeURIComponent(userID)}`
} }
const match = mxc.match(mediaRegex) return `_gomuks/media/${server}/${mediaID}`
if (!match) { // return `_gomuks/avatar/${encodeURIComponent(userID)}/${server}/${mediaID}`
return undefined
// return `_gomuks/avatar/${encodeURIComponent(userID)}`
}
return `_gomuks/media/${match[1]}/${match[2]}`
// return `_gomuks/avatar/${encodeURIComponent(userID)}/${match[1]}/${match[2]}`
} }

View file

@ -17,6 +17,7 @@ import React, { use, useCallback } from "react"
import { getAvatarURL } from "@/api/media.ts" import { getAvatarURL } from "@/api/media.ts"
import { RoomStateStore } from "@/api/statestore" import { RoomStateStore } from "@/api/statestore"
import { MemDBEvent, MemberEventContent } from "@/api/types" import { MemDBEvent, MemberEventContent } from "@/api/types"
import { isEventID } from "@/util/validation.ts"
import { ClientContext } from "../ClientContext.ts" import { ClientContext } from "../ClientContext.ts"
import { LightboxContext } from "../Lightbox.tsx" import { LightboxContext } from "../Lightbox.tsx"
import { ReplyIDBody } from "./ReplyBody.tsx" import { ReplyIDBody } from "./ReplyBody.tsx"
@ -152,8 +153,7 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyTo }: TimelineEventProps) =
<span className="event-time" title={editTime ? `${fullTime} - ${editTime}` : fullTime}>{shortTime}</span> <span className="event-time" title={editTime ? `${fullTime} - ${editTime}` : fullTime}>{shortTime}</span>
</div> </div>
<div className="event-content"> <div className="event-content">
{typeof replyTo === "string" && BodyType !== HiddenEvent {isEventID(replyTo) && BodyType !== HiddenEvent ? <ReplyIDBody room={room} eventID={replyTo}/> : null}
? <ReplyIDBody room={room} eventID={replyTo}/> : null}
<BodyType room={room} sender={memberEvt} event={evt}/> <BodyType room={room} sender={memberEvt} event={evt}/>
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null} {evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
</div> </div>

View file

@ -0,0 +1,51 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { ContentURI, EventID, RoomAlias, RoomID, UserID } from "@/api/types"
const simpleHomeserverRegex = /^[a-zA-Z0-9.:-]+$/
const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/
function isIdentifier<T>(identifier: unknown, sigil: string, requiresServer: boolean): identifier is T {
if (typeof identifier !== "string" || !identifier.startsWith(sigil)) {
return false
}
if (requiresServer) {
const idx = identifier.indexOf(":")
return idx > 0 && simpleHomeserverRegex.test(identifier.slice(idx+1))
}
return true
}
export function validated<T>(value: T | undefined, validator: (value: T) => boolean): value is T {
return value !== undefined && validator(value)
}
export const isEventID = (eventID: unknown) => isIdentifier<EventID>(eventID, "$", false)
export const isUserID = (userID: unknown) => isIdentifier<UserID>(userID, "@", true)
export const isRoomID = (roomID: unknown) => isIdentifier<RoomID>(roomID, "!", true)
export const isRoomAlias = (roomAlias: unknown) => isIdentifier<RoomAlias>(roomAlias, "#", true)
export const isMXC = (mxc: unknown): mxc is ContentURI => typeof mxc === "string" && mediaRegex.test(mxc)
export function parseMXC(mxc: unknown): [string, string] | [] {
if (typeof mxc !== "string") {
return []
}
const match = mxc.match(mediaRegex)
if (!match) {
return []
}
return [match[1], match[2]]
}