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,      };  };  | 
