summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRaphael Kabo <raphaelkabo@hey.com>2024-02-25 21:34:33 +0000
committerRaphael Kabo <raphaelkabo@hey.com>2024-02-26 00:11:03 +0000
commitb3c9cba6478dc16d135313aa6d0adcc02d67ece6 (patch)
tree508ef236dc805b950fedeeca7f8dd3559d66a162 /src
parentafd9fc4477fff90e5db917f350d99c3d01fba2bd (diff)
feat: optional public events/groups
Diffstat (limited to 'src')
-rw-r--r--src/lib/config.ts66
-rw-r--r--src/models/Event.ts5
-rwxr-xr-xsrc/models/EventGroup.ts5
-rw-r--r--src/routes/event.ts2
-rw-r--r--src/routes/frontend.ts71
-rw-r--r--src/routes/group.ts2
-rw-r--r--src/util/validation.ts31
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,
};
};