out-of-your-element/matrix/api.js

264 lines
8.1 KiB
JavaScript
Raw Normal View History

2023-05-08 05:22:20 +00:00
// @ts-check
2023-07-04 05:19:17 +00:00
const Ty = require("../types")
2023-09-25 04:36:27 +00:00
const assert = require("assert").strict
2023-05-08 05:22:20 +00:00
const passthrough = require("../passthrough")
const { discord, sync, db } = passthrough
/** @type {import("./mreq")} */
const mreq = sync.require("./mreq")
/** @type {import("./file")} */
const file = sync.require("./file")
/** @type {import("./txnid")} */
const makeTxnId = sync.require("./txnid")
/**
* @param {string} p endpoint to access
* @param {string?} [mxid] optional: user to act as, for the ?user_id parameter
2023-07-28 05:05:13 +00:00
* @param {{[x: string]: any}} [otherParams] optional: any other query parameters to add
* @returns {string} the new endpoint
*/
2023-07-28 05:05:13 +00:00
function path(p, mxid, otherParams = {}) {
2023-08-21 09:04:41 +00:00
const u = new URL(p, "http://localhost")
if (mxid) u.searchParams.set("user_id", mxid)
2023-08-21 09:04:41 +00:00
for (const entry of Object.entries(otherParams)) {
if (entry[1] != undefined) {
u.searchParams.set(entry[0], entry[1])
}
}
let result = u.pathname
const str = u.searchParams.toString()
if (str) result += "?" + str
return result
}
2023-05-08 05:22:20 +00:00
/**
* @param {string} username
2023-07-04 05:19:17 +00:00
* @returns {Promise<Ty.R.Registered>}
2023-05-08 05:22:20 +00:00
*/
function register(username) {
2023-08-21 09:04:41 +00:00
console.log(`[api] register: ${username}`)
return mreq.mreq("POST", "/client/v3/register", {
type: "m.login.application_service",
username
})
2023-05-08 05:22:20 +00:00
}
/**
* @returns {Promise<string>} room ID
*/
async function createRoom(content) {
2023-08-21 09:04:41 +00:00
console.log(`[api] create room:`, content)
/** @type {Ty.R.RoomCreated} */
const root = await mreq.mreq("POST", "/client/v3/createRoom", content)
return root.room_id
}
/**
* @returns {Promise<string>} room ID
*/
async function joinRoom(roomIDOrAlias, mxid) {
2023-08-21 09:04:41 +00:00
/** @type {Ty.R.RoomJoined} */
const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid))
return root.room_id
}
async function inviteToRoom(roomID, mxidToInvite, mxid) {
2023-08-21 09:04:41 +00:00
await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/invite`, mxid), {
user_id: mxidToInvite
})
}
async function leaveRoom(roomID, mxid) {
2023-08-21 09:04:41 +00:00
await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {})
}
/**
* @param {string} roomID
* @param {string} eventID
* @template T
*/
async function getEvent(roomID, eventID) {
2023-08-21 09:04:41 +00:00
/** @type {Ty.Event.Outer<T>} */
const root = await mreq.mreq("GET", `/client/v3/rooms/${roomID}/event/${eventID}`)
return root
}
/**
* @param {string} roomID
* @param {number} ts unix silliseconds
*/
async function getEventForTimestamp(roomID, ts) {
/** @type {{event_id: string, origin_server_ts: number}} */
const root = await mreq.mreq("GET", path(`/client/v1/rooms/${roomID}/timestamp_to_event`, null, {ts}))
return root
}
/**
* @param {string} roomID
2023-07-04 05:19:17 +00:00
* @returns {Promise<Ty.Event.BaseStateEvent[]>}
*/
function getAllState(roomID) {
2023-08-21 09:04:41 +00:00
return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state`)
}
/**
* @param {string} roomID
* @param {string} type
* @param {string} key
* @returns the *content* of the state event
*/
function getStateEvent(roomID, type, key) {
return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state/${type}/${key}`)
}
/**
* "Any of the AS's users must be in the room. This API is primarily for Application Services and should be faster to respond than /members as it can be implemented more efficiently on the server."
* @param {string} roomID
2024-02-09 04:29:05 +00:00
* @returns {Promise<{joined: {[mxid: string]: {avatar_url: string?, display_name: string?}}}>}
*/
function getJoinedMembers(roomID) {
2023-08-21 09:04:41 +00:00
return mreq.mreq("GET", `/client/v3/rooms/${roomID}/joined_members`)
}
/**
* @param {string} roomID
* @param {{from?: string, limit?: any}} pagination
* @returns {Promise<Ty.HierarchyPagination<Ty.R.Hierarchy>>}
*/
function getHierarchy(roomID, pagination) {
let path = `/client/v1/rooms/${roomID}/hierarchy`
if (!pagination.from) delete pagination.from
if (!pagination.limit) pagination.limit = 50
path += `?${new URLSearchParams(pagination)}`
return mreq.mreq("GET", path)
}
2023-09-25 04:36:27 +00:00
/**
* @param {string} roomID
* @param {string} eventID
* @param {{from?: string, limit?: any}} pagination
2023-09-25 04:36:27 +00:00
* @param {string?} [relType]
* @returns {Promise<Ty.Pagination<Ty.Event.Outer<any>>>}
*/
function getRelations(roomID, eventID, pagination, relType) {
2023-09-25 04:36:27 +00:00
let path = `/client/v1/rooms/${roomID}/relations/${eventID}`
if (relType) path += `/${relType}`
if (!pagination.from) delete pagination.from
if (!pagination.limit) pagination.limit = 50 // get a little more consistency between homeservers
path += `?${new URLSearchParams(pagination)}`
2023-09-25 04:36:27 +00:00
return mreq.mreq("GET", path)
}
/**
* @param {string} roomID
* @param {string} type
* @param {string} stateKey
* @param {string} [mxid]
* @returns {Promise<string>} event ID
*/
async function sendState(roomID, type, stateKey, content, mxid) {
2023-08-21 09:04:41 +00:00
console.log(`[api] state: ${roomID}: ${type}/${stateKey}`)
assert.ok(type)
assert.ok(typeof stateKey === "string")
/** @type {Ty.R.EventSent} */
2023-09-06 23:53:10 +00:00
// encodeURIComponent is necessary because state key can contain some special characters like / but you must encode them so they fit in a single component of the URI
const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/state/${type}/${encodeURIComponent(stateKey)}`, mxid), content)
2023-08-21 09:04:41 +00:00
return root.event_id
}
2023-07-28 05:05:13 +00:00
/**
* @param {string} roomID
* @param {string} type
* @param {any} content
* @param {string?} [mxid]
2023-07-28 05:05:13 +00:00
* @param {number} [timestamp] timestamp of the newly created event, in unix milliseconds
*/
async function sendEvent(roomID, type, content, mxid, timestamp) {
2023-09-23 06:12:51 +00:00
if (!["m.room.message", "m.reaction", "m.sticker"].includes(type)) {
console.log(`[api] event ${type} to ${roomID} as ${mxid || "default sim"}`)
}
2023-08-21 09:04:41 +00:00
/** @type {Ty.R.EventSent} */
const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/send/${type}/${makeTxnId.makeTxnId()}`, mxid, {ts: timestamp}), content)
return root.event_id
}
/**
* @param {string} roomID
* @param {string} eventID
* @param {string?} [mxid]
* @returns {Promise<string>} event ID
*/
async function redactEvent(roomID, eventID, mxid) {
2023-08-21 09:04:41 +00:00
/** @type {Ty.R.EventRedacted} */
const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/redact/${eventID}/${makeTxnId.makeTxnId()}`, mxid), {})
return root.event_id
}
2023-09-17 11:07:33 +00:00
/**
* @param {string} roomID
* @param {boolean} isTyping
* @param {string} mxid
* @param {number} [duration] milliseconds
*/
async function sendTyping(roomID, isTyping, mxid, duration) {
await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/typing/${mxid}`, mxid), {
typing: isTyping,
timeout: duration
2023-09-17 11:07:33 +00:00
})
}
2023-05-10 11:17:37 +00:00
async function profileSetDisplayname(mxid, displayname) {
2023-08-21 09:04:41 +00:00
await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid), {
displayname
})
2023-05-10 11:17:37 +00:00
}
async function profileSetAvatarUrl(mxid, avatar_url) {
2023-08-21 09:04:41 +00:00
await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/avatar_url`, mxid), {
avatar_url
})
}
/**
* Set a user's power level within a room.
* @param {string} roomID
* @param {string} mxid
* @param {number} power
*/
async function setUserPower(roomID, mxid, power) {
assert(roomID[0] === "!")
assert(mxid[0] === "@")
// Yes there's no shortcut https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352
2023-08-21 09:04:41 +00:00
const powerLevels = await getStateEvent(roomID, "m.room.power_levels", "")
2023-09-02 22:47:01 +00:00
powerLevels.users = powerLevels.users || {}
2023-08-21 09:04:41 +00:00
if (power != null) {
2023-09-02 22:47:01 +00:00
powerLevels.users[mxid] = power
2023-08-21 09:04:41 +00:00
} else {
2023-09-02 22:47:01 +00:00
delete powerLevels.users[mxid]
2023-08-21 09:04:41 +00:00
}
await sendState(roomID, "m.room.power_levels", "", powerLevels)
return powerLevels
2023-05-10 11:17:37 +00:00
}
module.exports.path = path
module.exports.register = register
module.exports.createRoom = createRoom
module.exports.joinRoom = joinRoom
module.exports.inviteToRoom = inviteToRoom
module.exports.leaveRoom = leaveRoom
module.exports.getEvent = getEvent
module.exports.getEventForTimestamp = getEventForTimestamp
module.exports.getAllState = getAllState
2023-08-24 05:09:25 +00:00
module.exports.getStateEvent = getStateEvent
module.exports.getJoinedMembers = getJoinedMembers
module.exports.getHierarchy = getHierarchy
2023-09-25 04:36:27 +00:00
module.exports.getRelations = getRelations
module.exports.sendState = sendState
module.exports.sendEvent = sendEvent
module.exports.redactEvent = redactEvent
2023-09-17 11:07:33 +00:00
module.exports.sendTyping = sendTyping
2023-05-10 11:17:37 +00:00
module.exports.profileSetDisplayname = profileSetDisplayname
module.exports.profileSetAvatarUrl = profileSetAvatarUrl
2023-08-21 09:04:41 +00:00
module.exports.setUserPower = setUserPower