Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a fork of the bot SDK #253

Draft
wants to merge 34 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5ac1b52
Add a FOSDEM JSON schedule loader
reivilibre Dec 16, 2024
0594690
Add test for a FOSDEM JSON schedule
reivilibre Dec 17, 2024
512a9ae
Add schema fields for `extraPeople`
reivilibre Nov 28, 2024
0abd0fe
Add the `extraPeople` to `getPeopleForAuditorium`
reivilibre Nov 28, 2024
da35e1c
Populate `extraPeople` for FOSDEM JSON schedules
reivilibre Dec 17, 2024
dd48e31
Add ability to provide custom request headers when fetching schedule
reivilibre Dec 19, 2024
df9b7c8
Merge branch 'rei/fd25_fosdempmatrix_format' into rei/fd25_BUILD_STAGING
reivilibre Dec 19, 2024
b4cda11
Add ability to sort auditoria into subspaces by track type
reivilibre Dec 27, 2024
9744453
Make the default invite command include support rooms
reivilibre Dec 27, 2024
8a445a6
Merge branch 'rei/fd25_subspaces_by_tracktypes' into rei/fd25_BUILD_S…
reivilibre Dec 27, 2024
491e30c
Merge branch 'rei/fd25_invite_incl_support' into rei/fd25_BUILD_STAGING
reivilibre Dec 27, 2024
08f7814
Add support for the `online_qa` field
reivilibre Dec 27, 2024
3ea6381
Remove `async` from `Auditorium.getDefinition`
reivilibre Jan 13, 2025
8f18743
Remove `async` from `Auditorium` methods `getId`, `getSlug`, `getName`
reivilibre Jan 13, 2025
b8fa147
Remove `async` from `Talk` getters
reivilibre Jan 13, 2025
f8b9d84
verify, attendance: accept auditorium slugs
reivilibre Jan 21, 2025
b809659
Merge branch 'rei/fd25_verifybyaudslug' into rei/fd25_BUILD_STAGING
reivilibre Jan 21, 2025
a67686b
Mark the attendance command as broken
reivilibre Jan 21, 2025
a193e29
Reimplement `findPeopleWithId` in an attempt to fix stored person ove…
reivilibre Jan 21, 2025
fc7c9a6
Merge branch 'rei/fd25_att_broken_maybefix_spoverrides' into rei/fd25…
reivilibre Jan 21, 2025
9f678e0
Support `!c` as a command shorthand
reivilibre Jan 21, 2025
0cd4810
Merge branch 'rei/fd25_shorthand' into rei/fd25_BUILD_STAGING
reivilibre Jan 21, 2025
89f1ffd
invite: Refresh the schedule if we can
reivilibre Jan 21, 2025
bcb4231
Merge branch 'rei/fd25_invite_refreshes' into rei/fd25_BUILD_STAGING
reivilibre Jan 21, 2025
9ab3fae
Merge branch 'rei/fd25_verifybyaudslug' into rei/fd25_vidstreams
reivilibre Jan 21, 2025
2856d16
Introduce 'livestreamId' on auditoria
reivilibre Jan 21, 2025
a921115
Expose `livestreamId` from auditoria to the video stream widget
reivilibre Jan 21, 2025
b2f57e2
Merge branch 'rei/fd25_vidstreams' into rei/fd25_BUILD_STAGING
reivilibre Jan 21, 2025
cc6bf84
Don't store one stored person override per redeemed 3pid invite
reivilibre Jan 21, 2025
5c13577
Only store person overrides if we are the bot process
reivilibre Jan 21, 2025
f8e3bd1
Merge branch 'rei/fd25_att_broken_maybefix_spoverrides' into rei/fd25…
reivilibre Jan 21, 2025
d4c102f
Add more diagnostics to the verify command
reivilibre Jan 23, 2025
10e8a08
Allow Matrix-inviting users even if they were previously e-mail-invited
reivilibre Jan 23, 2025
9fd4005
Use fork of matrix-bot-sdk
reivilibre Jan 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
FROM node:18-slim AS builder
FROM node:18 AS builder

# because yarn insists on using SSH to fetch the git repository, even with `git+https://`...
# seriously, why?!
RUN git config --global url."https://github".insteadOf ssh://git@github && git config --global url."https://github.com/".insteadOf git@github.com:

COPY ./ /app/
WORKDIR /app
Expand All @@ -8,7 +12,10 @@ RUN yarn install
ENV NODE_ENV=production
RUN yarn build

FROM node:18-slim
FROM node:18

# see note in builder stage
RUN git config --global url."https://github".insteadOf ssh://git@github && git config --global url."https://github.com/".insteadOf git@github.com:

COPY --from=builder /app/lib /app/lib
COPY --from=builder /app/srv /app/srv
Expand Down
5 changes: 4 additions & 1 deletion config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ livestream:
# The template for livestreams in auditorium rooms
# Available variables:
# id - The room ID/name (eg: "D.collab")
# sId - The room ID/name, but lowercase and only alphanumeric characters
# sId - [DEPRECATED] The room ID/name, but lowercase and only alphanumeric characters
# are included (eg: "dcollab")
# livestreamId - The livestream ID acquired from the conference schedule source.
auditoriumUrl: "https://stream.example.org/conference/hls/{id}.m3u8"

# The template for livestreams in talk rooms
Expand Down Expand Up @@ -226,6 +227,8 @@ conference:
# alias: stands
# # The prefixes of rooms which belong in the subspace
# prefixes: ["S."]
# # The types of tracks which belong in the subspace
# trackTypes: ["stands"]

# Options related to the IRC bridge. Set to null if you don't use an IRC bridge.
ircBridge: null
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"dependencies": {
"@tsconfig/node18": "^18.2.2",
"matrix-bot-sdk": "git+https://github.com/reivilibre/fork-matrix-bot-sdk#rei/fd25_dms-catch",
"await-lock": "^2.1.0",
"config": "^3.3.3",
"express": "^4.17.1",
Expand All @@ -33,7 +34,6 @@
"js-yaml": "^3.14.1",
"jsrsasign": "^10.1.4",
"liquidjs": "^9.19.0",
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.6.7-element.1",
"matrix-widget-api": "^1.6.0",
"moment": "^2.29.4",
"node-fetch": "^2.6.1",
Expand Down
115 changes: 90 additions & 25 deletions src/Conference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
} from "./models/room_state";
import { applySuffixRules, objectFastClone, safeCreateRoom } from "./utils";
import { addAndDeleteManagedAliases, applyAllAliasPrefixes, assignAliasVariations, calculateAliasVariations } from "./utils/aliases";
import { IConfig } from "./config";
import { IConfig, RunMode } from "./config";
import { MatrixRoom } from "./models/MatrixRoom";
import { Auditorium, AuditoriumBackstage } from "./models/Auditorium";
import { Talk } from "./models/Talk";
Expand Down Expand Up @@ -101,7 +101,8 @@ export class Conference {
// On any member event, recaulculate the membership.
this.enqueueRecalculateRoomMembership(roomId);

if (event['content']?.['third_party_invite']) {
// Only process 3pid invites when running as the bot, not the web interface
if (event['content']?.['third_party_invite'] && config.mode === RunMode.normal) {
const emailInviteToken = event['content']['third_party_invite']['signed']?.['token'];
const emailInvite = await this.client.getRoomStateEvent(roomId, "m.room.third_party_invite", emailInviteToken);
if (emailInvite[RS_3PID_PERSON_ID]) {
Expand All @@ -117,12 +118,11 @@ export class Conference {
const people = await this.findPeopleWithId(emailInvite[RS_3PID_PERSON_ID]);
if (people?.length) {
// Finally, associate the users.
for (const person of people) {
const clonedPerson = objectFastClone(person);
clonedPerson.matrix_id = event['state_key'];
await this.createUpdatePerson(clonedPerson);
LogService.info("Conference", `Updated ${clonedPerson.id} to be associated with ${clonedPerson.matrix_id}`);
}
let person = people[0];
const clonedPerson = objectFastClone(person);
clonedPerson.matrix_id = event['state_key'];
await this.createUpdatePerson(clonedPerson);
LogService.info("Conference", `Updated ${clonedPerson.id} to be associated with ${clonedPerson.matrix_id}`);

// Update permissions while we're here (if we can identify the room kind)
const aud = this.storedAuditoriums.find(a => a.roomId === roomId);
Expand Down Expand Up @@ -169,6 +169,8 @@ export class Conference {
* Returns all detected talk rooms for this conference.
* (Note that since physical auditoriums don't have any talk rooms, there won't be any results for talks
* in physical auditoriums here.)
*
* @deprecated as non-physical auditoria are not supported, this is now redundant
*/
public get storedTalks(): Talk[] {
return Object.values(this.talks);
Expand Down Expand Up @@ -621,7 +623,7 @@ export class Conference {
creation_content: {
[RSC_CONFERENCE_ID]: this.id,
[RSC_TALK_ID]: talk.id,
[RSC_AUDITORIUM_ID]: await auditorium.getId(),
[RSC_AUDITORIUM_ID]: auditorium.getId(),
},
initial_state: [
makeTalkLocator(this.id, talk.id),
Expand Down Expand Up @@ -672,6 +674,19 @@ export class Conference {

/**
* Determines the space in which an auditorium space or interest room should reside.
*
* For both auditoria and interest rooms, this is based off a set of configured prefixes for the
* auditorium or interest room ID.
*
* For auditoria, there is the additional option to match on the track type (with a set of configured
* mappings).
*
* ## Historical notes
*
* Matching on track types was added for FOSDEM 2025. Previously, only prefixes were available
* as a matching mechanism but the track type support was needed once auditoria were changed to
* represent tracks instead of being 1:1 mapped to physical in-person rooms.
*
* @param auditoriumOrInterestRoom The description of the auditorium or interest room.
* @returns The space in which the auditorium or interest room should reside.
*/
Expand All @@ -686,12 +701,19 @@ export class Conference {
const id = auditoriumOrInterestRoom.id;

for (const [subspaceId, subspaceConfig] of Object.entries(this.config.conference.subspaces)) {
for (const prefix of subspaceConfig.prefixes) {
if (id.startsWith(prefix)) {
if (!(subspaceId in this.subspaces)) {
throw new Error(`The ${subspaceId} subspace has not been created yet!`);
if (subspaceConfig.prefixes !== undefined) {
for (const prefix of subspaceConfig.prefixes) {
if (id.startsWith(prefix)) {
if (!(subspaceId in this.subspaces)) {
throw new Error(`The ${subspaceId} subspace has not been created yet!`);
}
return this.subspaces[subspaceId];
}
}
}

if (subspaceConfig.trackTypes !== undefined && 'trackType' in auditoriumOrInterestRoom) {
if (subspaceConfig.trackTypes.includes(auditoriumOrInterestRoom.trackType)) {
return this.subspaces[subspaceId];
}
}
Expand All @@ -706,23 +728,17 @@ export class Conference {
}

public async getPeopleForAuditorium(auditorium: Auditorium): Promise<IPerson[]> {
const audit = await auditorium.getDefinition();
const audit = auditorium.getDefinition();
const people: IPerson[] = [];
for (const t of this.backend.talks.values()) {
if (t.auditoriumId == audit.id) {
people.push(...t.speakers);
}
}
people.push(...audit.extraPeople);
return people;
}

/**
* @deprecated Just use `.getSpeakers()`
*/
public async getPeopleForTalk(talk: Talk): Promise<IPerson[]> {
return talk.getSpeakers();
}

/**
* @deprecated This always returns `[]`.
*/
Expand Down Expand Up @@ -760,7 +776,7 @@ export class Conference {
}

public async getInviteTargetsForTalk(talk: Talk): Promise<IPerson[]> {
const people = await this.getPeopleForTalk(talk);
const people = talk.getSpeakers();
const roles = [Role.Speaker, Role.Host, Role.Coordinator];
return people.filter(p => roles.includes(p.role));
}
Expand All @@ -778,7 +794,7 @@ export class Conference {
}

public async getModeratorsForTalk(talk: Talk): Promise<IPerson[]> {
const people = await this.getPeopleForTalk(talk);
const people = talk.getSpeakers();
const roles = [Role.Coordinator, Role.Speaker, Role.Host];
return people.filter(p => roles.includes(p.role));
}
Expand Down Expand Up @@ -811,6 +827,15 @@ export class Conference {
return this.auditoriums[audId];
}

public getAuditoriumBySlug(audSlug: string): Auditorium | null {
for (let auditorium of Object.values(this.auditoriums)) {
if (auditorium.getSlug() === audSlug) {
return auditorium;
}
}
return null;
}

public getAuditoriumBackstage(audId: string): AuditoriumBackstage {
return this.auditoriumBackstages[audId];
}
Expand All @@ -823,6 +848,25 @@ export class Conference {
return this.interestRooms[intId];
}

public getInterestById(audSlug: string): InterestRoom | null {
for (let interest of Object.values(this.interestRooms)) {
if (interest.getId() === audSlug) {
return interest;
}
}
return null;
}

public getAuditoriumOrInterestByIdOrSlug(audOrInterestIdOrSlug: string): Auditorium | InterestRoom | null {
if (this.auditoriums[audOrInterestIdOrSlug]) {
return this.auditoriums[audOrInterestIdOrSlug];
}
if (this.interestRooms[audOrInterestIdOrSlug]) {
return this.interestRooms[audOrInterestIdOrSlug];
}
return this.getAuditoriumBySlug(audOrInterestIdOrSlug);
}

public async ensurePermissionsFor(people: ResolvedPersonIdentifier[], roomId: string): Promise<void> {
const mxids = people.filter(t => !!t.mxid).map(r => r.mxid!);

Expand Down Expand Up @@ -875,10 +919,31 @@ export class Conference {
}

/**
* @deprecated This always returns `[]` and should be removed or fixed.
* Return a list of all people with the given person ID.
*
* TODO does not support interest rooms
*/
public async findPeopleWithId(personId: string): Promise<IPerson[]> {
return [];
let out: IPerson[] = [];

for (let auditorium of Object.values(this.auditoriums)) {
let audDef = auditorium.getDefinition();
for (let talk of audDef.talks.values()) {
for (let person of talk.speakers) {
if (person.id === personId) {
out.push(person);
}
}
}

for (let person of audDef.extraPeople) {
if (person.id === personId) {
out.push(person);
}
}
}

return out;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/IRCBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ export class IRCBridge {
}

public async deriveChannelName(auditorium: Auditorium) {
const name = await auditorium.getName();
const name = auditorium.getName();
if (!name) {
throw Error('Auditorium name is empty');
}
return `${this.config.channelPrefix}${name}`;
}

public async deriveChannelNameSI(interest: InterestRoom) {
const name = makeLocalpart(await interest.getName(), this.rootConfig.conference.prefixes.suffixes, await interest.getId());
const name = makeLocalpart(interest.getName(), this.rootConfig.conference.prefixes.suffixes, interest.getId());
if (!name) {
throw Error('Special interest name is empty');
}
Expand Down
6 changes: 3 additions & 3 deletions src/Scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export class Scheduler {
const confAudBackstage = this.conference.getAuditoriumBackstage(task.talk.auditoriumId);

// If we don't have a talk room and the talk isn't physical, we're missing a talk room,=.
const isMissingTalkRoom = (!confTalk) && !(await confAud.getDefinition()).isPhysical;
const isMissingTalkRoom = (!confTalk) && !confAud.getDefinition().isPhysical;

if (isMissingTalkRoom) {
LogService.warn("Scheduler", `Skipping task ${task.id} - Cannot find talk room`);
Expand Down Expand Up @@ -533,7 +533,7 @@ export class Scheduler {
}
}
await this.client.sendHtmlText(confTalk.roomId, `<h3>Please check in.</h3><p>${pills.join(', ')} - It does not appear as though you are present for your talk. Please say something in this room.</p>`);
await this.client.sendHtmlText(confAudBackstage.roomId, `<h3>Required persons not checked in for upcoming talk</h3><p>Please track down the speakers for <b>${await confTalk.getName()}</b>.</p><p>Missing: ${pills.join(', ')}</p>`);
await this.client.sendHtmlText(confAudBackstage.roomId, `<h3>Required persons not checked in for upcoming talk</h3><p>Please track down the speakers for <b>${confTalk.getName()}</b>.</p><p>Missing: ${pills.join(', ')}</p>`);

const userIds = await this.conference.getInviteTargetsForTalk(confTalk);
const resolved = (await resolveIdentifiers(this.client, userIds)).filter(p => p.mxid).map(p => p.mxid!);
Expand Down Expand Up @@ -573,7 +573,7 @@ export class Scheduler {
const roomPill = await MentionPill.forRoom(confTalk.roomId, this.client);
await this.client.sendHtmlText(this.config.managementRoom, `<h3>Talk is missing speakers</h3><p>${roomPill.html} is missing one or more speakers: ${pills.join(', ')}</p><p>The talk starts in about 15 minutes.</p>`);
await this.client.sendHtmlText(confTalk.roomId, `<h3>@room - please check in.</h3><p>${pills.join(', ')} - It does not appear as though you are present for your talk. Please say something in this room. The conference staff have been notified.</p>`);
await this.client.sendHtmlText(confAudBackstage.roomId, `<h3>Required persons not checked in for upcoming talk</h3><p>Please track down the speakers for <b>${await confTalk.getName()}</b>. The conference staff have been notified.</p><p>Missing: ${pills.join(', ')}</p>`);
await this.client.sendHtmlText(confAudBackstage.roomId, `<h3>Required persons not checked in for upcoming talk</h3><p>Please track down the speakers for <b>${confTalk.getName()}</b>. The conference staff have been notified.</p><p>Missing: ${pills.join(', ')}</p>`);

const userIds = await this.conference.getInviteTargetsForTalk(confTalk);
const resolved = (await resolveIdentifiers(this.client, userIds)).filter(p => p.mxid).map(p => p.mxid!);
Expand Down
70 changes: 70 additions & 0 deletions src/__tests__/backends/json/JsonScheduleBackend.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { test, expect, afterEach, beforeEach, describe } from "@jest/globals";
import { Server, createServer } from "node:http";
import { AddressInfo } from "node:net";
import path from "node:path";
import * as fs from "fs";
import { IConfig, JsonScheduleFormat } from "../../../config";
import { JsonScheduleBackend } from "../../../backends/json/JsonScheduleBackend";

function getFixture(fixtureFile: string) {
return fs.readFileSync(path.join(__dirname, fixtureFile), "utf8");
}

function jsonScheduleServer() {
return new Promise<Server>((resolve) => {
const server = createServer((req, res) => {
if (req.url === "/schedule.json") {
res.writeHead(200);
const json = getFixture("original_democon.json");
res.end(json);
} else if (req.url === "/fosdem/p/matrix") {
if (req.headers.authorization !== "Bearer TOKEN") {
res.writeHead(401);
res.end("Not authorised");
return;
}
res.writeHead(200);
const json = getFixture("fosdem_democon.json");
res.end(json);
} else {
console.log(req.url);
res.writeHead(404);
res.end("Not found");
}
}).listen(undefined, "127.0.0.1", undefined, () => {
resolve(server);
});
});
}

describe("JsonScheduleBackend", () => {
let serv;
beforeEach(async () => {
serv = await jsonScheduleServer();
});
afterEach(async () => {
serv.close();
});

test("can parse a FOSDEM JSON format", async () => {
const globalConfig = { conference: { name: "DemoCon" } } as IConfig;
const backend = await JsonScheduleBackend.new(
"/dev/null",
{
backend: "json",
scheduleFormat: JsonScheduleFormat.FOSDEM,
scheduleDefinition: `http://127.0.0.1:${
(serv.address() as AddressInfo).port
}/fosdem/p/matrix`,
scheduleRequestHeaders: {
"Authorization": "Bearer TOKEN"
}
},
globalConfig
);
expect(backend.conference.title).toBe("DemoCon");
expect(backend.auditoriums).toMatchSnapshot("auditoriums");
expect(backend.talks).toMatchSnapshot("talks");
expect(backend.interestRooms.size).toBe(0);
});
});
Loading
Loading