diff options
author | Raphael Kabo <raphaelkabo@hey.com> | 2024-02-25 21:34:33 +0000 |
---|---|---|
committer | Raphael Kabo <raphaelkabo@hey.com> | 2024-02-26 00:11:03 +0000 |
commit | b3c9cba6478dc16d135313aa6d0adcc02d67ece6 (patch) | |
tree | 508ef236dc805b950fedeeca7f8dd3559d66a162 /src | |
parent | afd9fc4477fff90e5db917f350d99c3d01fba2bd (diff) |
feat: optional public events/groups
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/config.ts | 66 | ||||
-rw-r--r-- | src/models/Event.ts | 5 | ||||
-rwxr-xr-x | src/models/EventGroup.ts | 5 | ||||
-rw-r--r-- | src/routes/event.ts | 2 | ||||
-rw-r--r-- | src/routes/frontend.ts | 71 | ||||
-rw-r--r-- | src/routes/group.ts | 2 | ||||
-rw-r--r-- | src/util/validation.ts | 31 |
7 files changed, 173 insertions, 9 deletions
diff --git a/src/lib/config.ts b/src/lib/config.ts index 1029be9..b4385ca 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -14,10 +14,11 @@ interface GathioConfig { port: string; email: string; site_name: string; - delete_after_days: number | null; + delete_after_days: number; is_federated: boolean; email_logo_url: string; show_kofi: boolean; + show_public_event_list: boolean; mail_service: "nodemailer" | "sendgrid"; creator_email_addresses: string[]; }; @@ -42,6 +43,7 @@ interface FrontendConfig { isFederated: boolean; emailLogoUrl: string; showKofi: boolean; + showPublicEventList: boolean; showInstanceInformation: boolean; staticPages?: StaticPage[]; version: string; @@ -56,8 +58,10 @@ const defaultConfig: GathioConfig = { is_federated: true, delete_after_days: 7, email_logo_url: "", + show_public_event_list: false, show_kofi: false, mail_service: "nodemailer", + creator_email_addresses: [], }, database: { mongodb_url: "mongodb://localhost:27017/gathio", @@ -69,15 +73,71 @@ export const frontendConfig = (): FrontendConfig => { return { domain: config.general.domain, siteName: config.general.site_name, - isFederated: config.general.is_federated, + isFederated: !!config.general.is_federated, emailLogoUrl: config.general.email_logo_url, - showKofi: config.general.show_kofi, + showPublicEventList: !!config.general.show_public_event_list, + showKofi: !!config.general.show_kofi, showInstanceInformation: !!config.static_pages?.length, staticPages: config.static_pages, version: process.env.npm_package_version || "unknown", }; }; +interface InstanceRule { + icon: string; + text: string; +} + +export const instanceRules = (): InstanceRule[] => { + const config = getConfig(); + const rules = []; + rules.push( + config.general.show_public_event_list + ? { + text: "Public events and groups are displayed on the homepage", + icon: "fas fa-eye", + } + : { + text: "Events and groups can only be accessed by direct link", + icon: "fas fa-eye-slash", + }, + ); + rules.push( + config.general.creator_email_addresses?.length + ? { + text: "Only specific people can create events and groups", + icon: "fas fa-user-check", + } + : { + text: "Anyone can create events and groups", + icon: "fas fa-users", + }, + ); + rules.push( + config.general.delete_after_days > 0 + ? { + text: `Events are automatically deleted ${config.general.delete_after_days} days after they end`, + icon: "far fa-calendar-times", + } + : { + text: "Events are permanent, and are never automatically deleted", + icon: "far fa-calendar-check", + }, + ); + rules.push( + config.general.is_federated + ? { + text: "This instance federates with other instances using ActivityPub", + icon: "fas fa-globe", + } + : { + text: "This instance does not federate with other instances", + icon: "fas fa-globe", + }, + ); + return rules; +}; + // Attempt to load our global config. Will stop the app if the config file // cannot be read (there's no point trying to continue!) export const getConfig = (): GathioConfig => { diff --git a/src/models/Event.ts b/src/models/Event.ts index f67d40b..5731680 100644 --- a/src/models/Event.ts +++ b/src/models/Event.ts @@ -73,6 +73,7 @@ export interface IEvent extends mongoose.Document { privateKey?: string; followers?: IFollower[]; activityPubMessages?: IActivityPubMessage[]; + showOnPublicList?: boolean; } const Attendees = new mongoose.Schema({ @@ -334,6 +335,10 @@ const EventSchema = new mongoose.Schema({ }, followers: [Followers], activityPubMessages: [ActivityPubMessages], + showOnPublicList: { + type: Boolean, + default: false, + }, }); export default mongoose.model<IEvent>("Event", EventSchema); diff --git a/src/models/EventGroup.ts b/src/models/EventGroup.ts index 2b5c2aa..de7187d 100755 --- a/src/models/EventGroup.ts +++ b/src/models/EventGroup.ts @@ -16,6 +16,7 @@ export interface IEventGroup extends mongoose.Document { firstLoad?: boolean; events?: mongoose.Types.ObjectId[]; subscribers?: ISubscriber[]; + showOnPublicList?: boolean; } const Subscriber = new mongoose.Schema({ @@ -70,6 +71,10 @@ const EventGroupSchema = new mongoose.Schema({ }, events: [{ type: mongoose.Schema.Types.ObjectId, ref: "Event" }], subscribers: [Subscriber], + showOnPublicList: { + type: Boolean, + default: false, + }, }); export default mongoose.model<IEventGroup>("EventGroup", EventGroupSchema); diff --git a/src/routes/event.ts b/src/routes/event.ts index fb9d8c7..6be5ff8 100644 --- a/src/routes/event.ts +++ b/src/routes/event.ts @@ -140,6 +140,7 @@ router.post( viewPassword: "", // Backwards compatibility editPassword: "", // Backwards compatibility editToken: editToken, + showOnPublicList: eventData?.publicBoolean, eventGroup: isPartOfEventGroup ? eventGroup?._id : null, usersCanAttend: eventData.joinBoolean ? true : false, showUsersList: false, // Backwards compatibility @@ -371,6 +372,7 @@ router.put( url: eventData.eventURL, hostName: eventData.hostName, image: eventImageFilename, + showOnPublicList: eventData.publicBoolean, usersCanAttend: eventData.joinBoolean, showUsersList: false, // Backwards compatibility usersCanComment: eventData.interactionBoolean, diff --git a/src/routes/frontend.ts b/src/routes/frontend.ts index 0d8793a..4cdce8a 100644 --- a/src/routes/frontend.ts +++ b/src/routes/frontend.ts @@ -2,7 +2,7 @@ import { Router, Request, Response } from "express"; import moment from "moment-timezone"; import { marked } from "marked"; import { markdownToSanitizedHTML, renderPlain } from "../util/markdown.js"; -import getConfig, { frontendConfig } from "../lib/config.js"; +import getConfig, { frontendConfig, instanceRules } from "../lib/config.js"; import { addToLog, exportICal } from "../helpers.js"; import Event from "../models/Event.js"; import EventGroup, { IEventGroup } from "../models/EventGroup.js"; @@ -16,7 +16,20 @@ const config = getConfig(); const router = Router(); router.get("/", (_: Request, res: Response) => { - res.render("home", frontendConfig()); + if (config.general.show_public_event_list) { + return res.redirect("/events"); + } + return res.render("home", { + ...frontendConfig(), + instanceRules: instanceRules(), + }); +}); + +router.get("/about", (_: Request, res: Response) => { + return res.render("home", { + ...frontendConfig(), + instanceRules: instanceRules(), + }); }); router.get("/new", (_: Request, res: Response) => { @@ -57,6 +70,51 @@ router.get("/new/:magicLinkToken", async (req: Request, res: Response) => { }); }); +router.get("/events", async (_: Request, res: Response) => { + if (!config.general.show_public_event_list) { + return res.status(404).render("404", frontendConfig()); + } + const events = await Event.find({ showOnPublicList: true }) + .populate("eventGroup") + .lean() + .sort("start"); + const updatedEvents = events.map((event) => { + const startMoment = moment.tz(event.start, event.timezone); + const endMoment = moment.tz(event.end, event.timezone); + const isSameDay = startMoment.isSame(endMoment, "day"); + + return { + id: event.id, + name: event.name, + location: event.location, + displayDate: isSameDay + ? startMoment.format("D MMM YYYY") + : `${startMoment.format("D MMM YYYY")} - ${endMoment.format( + "D MMM YYYY", + )}`, + eventHasConcluded: endMoment.isBefore(moment.tz(event.timezone)), + eventGroup: event.eventGroup, + }; + }); + const upcomingEvents = updatedEvents.filter( + (event) => event.eventHasConcluded === false, + ); + const pastEvents = updatedEvents.filter( + (event) => event.eventHasConcluded === true, + ); + const eventGroups = await EventGroup.find({ + showOnPublicList: true, + }).lean(); + + res.render("publicEventList", { + title: "Public events", + upcomingEvents: upcomingEvents, + pastEvents: pastEvents, + eventGroups: eventGroups, + ...frontendConfig(), + }); +}); + router.get("/:eventID", async (req: Request, res: Response) => { try { const event = await Event.findOne({ @@ -266,6 +324,11 @@ router.get("/:eventID", async (req: Request, res: Response) => { firstLoad: firstLoad, eventHasConcluded: eventHasConcluded, eventHasBegun: eventHasBegun, + eventWillBeDeleted: config.general.delete_after_days > 0, + daysUntilDeletion: moment + .tz(event.end, event.timezone) + .add(config.general.delete_after_days, "days") + .fromNow(), metadata: metadata, jsonData: { name: event.name, @@ -276,6 +339,7 @@ router.get("/:eventID", async (req: Request, res: Response) => { url: event.url, hostName: event.hostName, creatorEmail: event.creatorEmail, + showOnPublicList: event.showOnPublicList, eventGroupID: event.eventGroup ? (event.eventGroup as unknown as IEventGroup).id : null, @@ -337,6 +401,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { return { id: event.id, name: event.name, + location: event.location, displayDate: isSameDay ? startMoment.format("D MMM YYYY") : `${startMoment.format("D MMM YYYY")} - ${endMoment.format( @@ -388,6 +453,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { res.set("X-Robots-Tag", "noindex"); res.render("eventgroup", { + ...frontendConfig(), domain: config.general.domain, title: eventGroup.name, eventGroupData: eventGroup, @@ -409,6 +475,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { creatorEmail: eventGroup.creatorEmail, image: eventGroup.image, editToken: editingEnabled ? eventGroupEditToken : null, + showOnPublicList: eventGroup.showOnPublicList, }, }); } catch (err) { diff --git a/src/routes/group.ts b/src/routes/group.ts index 34377b0..c006a5d 100644 --- a/src/routes/group.ts +++ b/src/routes/group.ts @@ -81,6 +81,7 @@ router.post( hostName: groupData.hostName, editToken: editToken, firstLoad: true, + showOnPublicList: groupData.publicBoolean, }); await eventGroup.save(); @@ -206,6 +207,7 @@ router.put( url: req.body.eventGroupURL, hostName: req.body.hostName, image: eventGroupImageFilename, + showOnPublicList: groupData.publicBoolean, }; await EventGroup.findOneAndUpdate( diff --git a/src/util/validation.ts b/src/util/validation.ts index 732fbf3..b9a0c8a 100644 --- a/src/util/validation.ts +++ b/src/util/validation.ts @@ -5,11 +5,16 @@ type Error = { field?: string; }; -type ValidationResponse = { +type EventValidationResponse = { data?: ValidatedEventData; errors?: Error[]; }; +type EventGroupValidationResponse = { + data?: ValidatedEventGroupData; + errors?: Error[]; +}; + interface EventData { eventName: string; eventLocation: string; @@ -21,6 +26,7 @@ interface EventData { imagePath: string; hostName: string; creatorEmail: string; + publicCheckbox: string; eventGroupCheckbox: string; eventGroupID: string; eventGroupEditToken: string; @@ -33,11 +39,13 @@ interface EventData { // EventData without the 'checkbox' fields export type ValidatedEventData = Omit< EventData, + | "publicCheckbox" | "eventGroupCheckbox" | "interactionCheckbox" | "joinCheckbox" | "maxAttendeesCheckbox" > & { + publicBoolean: boolean; eventGroupBoolean: boolean; interactionBoolean: boolean; joinBoolean: boolean; @@ -50,8 +58,13 @@ interface EventGroupData { eventGroupURL: string; hostName: string; creatorEmail: string; + publicCheckbox: string; } +export type ValidatedEventGroupData = Omit<EventGroupData, "publicCheckbox"> & { + publicBoolean: boolean; +}; + const validateEmail = (email: string) => { if (!email || email.length === 0 || typeof email !== "string") { return false; @@ -89,9 +102,12 @@ export const validateEventTime = (start: Date, end: Date): Error | boolean => { return true; }; -export const validateEventData = (eventData: EventData): ValidationResponse => { +export const validateEventData = ( + eventData: EventData, +): EventValidationResponse => { const validatedData: ValidatedEventData = { ...eventData, + publicBoolean: eventData.publicCheckbox === "true", eventGroupBoolean: eventData.eventGroupCheckbox === "true", interactionBoolean: eventData.interactionCheckbox === "true", joinBoolean: eventData.joinCheckbox === "true", @@ -186,7 +202,9 @@ export const validateEventData = (eventData: EventData): ValidationResponse => { }; }; -export const validateGroupData = (groupData: EventGroupData) => { +export const validateGroupData = ( + groupData: EventGroupData, +): EventGroupValidationResponse => { const errors: Error[] = []; if (!groupData.eventGroupName) { errors.push({ @@ -209,8 +227,13 @@ export const validateGroupData = (groupData: EventGroupData) => { } } + const validatedData: ValidatedEventGroupData = { + ...groupData, + publicBoolean: groupData.publicCheckbox === "true", + }; + return { - data: groupData, + data: validatedData, errors: errors, }; }; |