diff options
| author | Raphael <mail@raphaelkabo.com> | 2025-05-28 18:58:46 +0100 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-28 18:58:46 +0100 | 
| commit | 3d84891118f8a81af3ddb978af9b3f8b02fd5d65 (patch) | |
| tree | 0a8d344e331a0551b73435bbbb3919107737f69f /src/lib | |
| parent | 6f0b7a44b995b6b66baf42a9369182fc05a90b34 (diff) | |
| parent | 4664b6968fdcaca54268d60f400da02364213f05 (diff) | |
Merge branch 'main' into main
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/config.ts | 34 | ||||
| -rw-r--r-- | src/lib/email.ts | 71 | ||||
| -rw-r--r-- | src/lib/event.ts | 4 | ||||
| -rw-r--r-- | src/lib/middleware.ts | 2 | 
4 files changed, 85 insertions, 26 deletions
diff --git a/src/lib/config.ts b/src/lib/config.ts index 6642eef..35fc42c 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -3,6 +3,7 @@ import toml from "toml";  import { exitWithError } from "./process.js";  import { Response } from "express";  import { markdownToSanitizedHTML } from "../util/markdown.js"; +import i18next from "i18next";  interface StaticPage {      title: string; @@ -21,7 +22,7 @@ export interface GathioConfig {          email_logo_url: string;          show_kofi: boolean;          show_public_event_list: boolean; -        mail_service: "nodemailer" | "sendgrid" | "none"; +        mail_service: "nodemailer" | "sendgrid" | "mailgun" | "none";          creator_email_addresses: string[];      };      database: { @@ -37,6 +38,11 @@ export interface GathioConfig {      sendgrid?: {          api_key: string;      }; +    mailgun?: { +        api_key: string; +        api_url: string; +        domain: string; +    };      static_pages?: StaticPage[];  } @@ -110,44 +116,44 @@ export const instanceRules = (): InstanceRule[] => {      rules.push(          config.general.show_public_event_list              ? { -                text: "Public events and groups are displayed on the homepage", +                text: i18next.t("config.instancerule.showpubliceventlist-true"),                  icon: "fas fa-eye",              }              : { -                text: "Events and groups can only be accessed by direct link", +                text: i18next.t("config.instancerule.showpubliceventlist-false"),                  icon: "fas fa-eye-slash",              },      );      rules.push(          config.general.creator_email_addresses?.length              ? { -                text: "Only specific people can create events and groups", +                text: i18next.t("config.instancerule.creatoremail-true"),                  icon: "fas fa-user-check",              }              : { -                text: "Anyone can create events and groups", +                text: i18next.t("config.instancerule.creatoremail-false"),                  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`, +                text: i18next.t("config.instancerule.deleteafterdays-true", { days: config.general.delete_after_days } ),                  icon: "far fa-calendar-times",              }              : { -                text: "Events are permanent, and are never automatically deleted", +                text: i18next.t("config.instancerule.deleteafterdays-false"),                  icon: "far fa-calendar-check",              },      );      rules.push(          config.general.is_federated              ? { -                text: "This instance federates with other instances using ActivityPub", +                text: i18next.t("config.instancerule.isfederated-true"),                  icon: "fas fa-globe",              }              : { -                text: "This instance does not federate with other instances", +                text: i18next.t("config.instancerule.isfederated-false"),                  icon: "fas fa-globe",              },      ); @@ -156,13 +162,15 @@ export const instanceRules = (): InstanceRule[] => {  export const instanceDescription = (): string => {      const config = getConfig(); -    const defaultInstanceDescription = -        "**{{ siteName }}** is running on Gathio — a simple, federated, privacy-first event hosting platform."; +    const defaultInstanceDescription = markdownToSanitizedHTML( +        i18next.t("config.defaultinstancedesc", "Welcome to this Gathio instance!") +    );      let instanceDescription = defaultInstanceDescription; +    let instancedescfile = "./static/instance-description-" + i18next.language + ".md";      try { -        if (fs.existsSync("./static/instance-description.md")) { +        if (fs.existsSync(instancedescfile)) {              const fileBody = fs.readFileSync( -                "./static/instance-description.md", +                instancedescfile,                  "utf-8",              );              instanceDescription = markdownToSanitizedHTML(fileBody); diff --git a/src/lib/email.ts b/src/lib/email.ts index 7c7c2dd..fc585a1 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -5,6 +5,8 @@ import nodemailer, { Transporter } from "nodemailer";  import { GathioConfig, getConfig } from "./config.js";  import SMTPTransport from "nodemailer/lib/smtp-transport/index.js";  import { exitWithError } from "./process.js"; +import Mailgun from "mailgun.js"; +import { IMailgunClient } from "node_modules/mailgun.js/Types/Interfaces/index.js";  const config = getConfig(); @@ -24,7 +26,8 @@ type EmailTemplateName =  export class EmailService {      nodemailerTransporter: Transporter | undefined = undefined;      sgMail: typeof sgMail | undefined = undefined; -    hbs: ExpressHandlebars +    mailgunClient: IMailgunClient | undefined = undefined; +    hbs: ExpressHandlebars;      public constructor(config: GathioConfig, hbs: ExpressHandlebars) {          this.hbs = hbs; @@ -40,6 +43,26 @@ export class EmailService {                  console.log("Sendgrid is ready to send emails.");                  break;              } +            case "mailgun": { +                if ( +                    !config.mailgun?.api_key || +                    !config.mailgun?.api_url || +                    !config.mailgun?.domain +                ) { +                    return exitWithError( +                        "Mailgun is configured as the email service, but not all required fields are provided. Please provide all required fields in the config file.", +                    ); +                } +                const mailgun = new Mailgun(FormData); +                this.mailgunClient = mailgun.client({ +                    username: "api", +                    key: config.mailgun.api_key, +                    url: config.mailgun.api_url, +                }); +                // TODO: Can we verify the Mailgun connection? +                console.log("Mailgun is ready to send emails."); +                break; +            }              case "nodemailer": {                  if (config.nodemailer?.smtp_url) {                      this.nodemailerTransporter = nodemailer.createTransport( @@ -73,13 +96,13 @@ export class EmailService {                          nodemailer.createTransport(nodemailerConfig);                  }              } -          }      }      public async verify(): Promise<boolean> {          if (this.nodemailerTransporter) { -            const nodemailerVerified = await this.nodemailerTransporter.verify(); +            const nodemailerVerified = +                await this.nodemailerTransporter.verify();              if (nodemailerVerified) {                  console.log("Nodemailer is ready to send emails.");                  return true; @@ -118,9 +141,36 @@ export class EmailService {                  return true;              } catch (e: unknown | sgHelpers.classes.ResponseError) {                  if (e instanceof sgHelpers.classes.ResponseError) { -                    console.error('sendgrid error', e.response.body); +                    console.error("sendgrid error", e.response.body); +                } else { +                    console.error("sendgrid error", e); +                } +                return false; +            } +        } else if (this.mailgunClient) { +            try { +                if (!config.mailgun?.domain) { +                    return exitWithError( +                        "Mailgun is configured as the email service, but no domain is provided. Please provide a domain in the config file.", +                    ); +                } +                await this.mailgunClient.messages.create( +                    config.mailgun.domain, +                    { +                        from: config.general.email, +                        to, +                        bcc, +                        subject: `${config.general.site_name}: ${subject}`, +                        text, +                        html, +                    }, +                ); +                return true; +            } catch (e: any) { +                if (e.response) { +                    console.error(e.response.body);                  } else { -                    console.error('sendgrid error', e); +                    console.error(e);                  }                  return false;              } @@ -150,15 +200,14 @@ export class EmailService {          bcc = "",          subject,          templateName, -        templateData = {} +        templateData = {},      }: {          to: string | string[];          bcc?: string | string[] | undefined;          subject: string;          templateName: EmailTemplateName;          templateData?: object; -    }, -    ): Promise<boolean> { +    }): Promise<boolean> {          const [html, text] = await Promise.all([              this.hbs.renderView(                  `./views/emails/${templateName}/${templateName}Html.handlebars`, @@ -172,7 +221,7 @@ export class EmailService {                      cache: true,                      layout: "email.handlebars",                      ...templateData, -                } +                },              ),              this.hbs.renderView(                  `./views/emails/${templateName}/${templateName}Text.handlebars`, @@ -186,7 +235,7 @@ export class EmailService {                      cache: true,                      layout: "email.handlebars",                      ...templateData, -                } +                },              ),          ]); @@ -195,7 +244,7 @@ export class EmailService {              bcc,              subject: `${config.general.site_name}: ${subject}`,              text, -            html +            html,          });      }  } diff --git a/src/lib/event.ts b/src/lib/event.ts index 334ddf6..bcb7cd9 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -1,3 +1,4 @@ +import i18next from "i18next";  import { IEventGroup } from "../models/EventGroup.js";  export interface EventListEvent { @@ -15,7 +16,8 @@ export const bucketEventsByMonth = (      acc: Record<string, any>[],      event: EventListEvent,  ) => { -    const month = event.startMoment.format("MMMM YYYY"); +    event.startMoment.locale(i18next.language); +    const month = event.startMoment.format(i18next.t("common.year-month-format" ));      const matchingBucket = acc.find((bucket) => bucket.title === month);      if (!matchingBucket) {          acc.push({ diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 69fbe4e..be05c3f 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -62,7 +62,7 @@ export const getConfigMiddleware = (      if (process.env.CYPRESS === "true" && req.cookies?.cypressConfigOverride) {          console.log("Overriding config with Cypress config");          const override = JSON.parse(req.cookies.cypressConfigOverride); -        res.locals.config = deepMerge<GathioConfig>(config, override); +        res.locals.config = deepMerge(config, override);          return next();      }      res.locals.config = config;  | 
