Fix calendar caching

This commit is contained in:
Fergal Moran
2023-02-28 01:34:24 +00:00
parent cc94bbf488
commit 0ad595d4fd
25 changed files with 480 additions and 175 deletions

View File

@@ -9,4 +9,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Cache outstanding events - name: Cache outstanding events
run: curl -X GET https://otherway.fergl.ie/api/cron/cache uses: wei/curl@master
with:
args: https://otherway.fergl.ie/api/cron/cache

View File

@@ -9,4 +9,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Cache outstanding events - name: Cache outstanding events
run: curl -X GET https://otherway.fergl.ie/api/cron/reminders uses: wei/curl@master
with:
args: https://otherway.fergl.ie/api/cron/reminders

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { themeChange } from "theme-change";
import "./globals.css"; import "./globals.css";
import { Raleway } from "@next/font/google"; import { Raleway } from "@next/font/google";
import { NavBar } from "@/components/layout"; import { NavBar } from "@/components/layout";
import { AuthUserProvider } from "@/lib/auth/authUserContext"; import { AuthUserProvider } from "@/lib/auth/authUserContext";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { defaults } from "@/lib/constants";
const font = Raleway({ const font = Raleway({
weight: ["400", "700"], weight: ["400", "700"],
@@ -19,7 +19,10 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
React.useEffect(() => { React.useEffect(() => {
themeChange(false); const theme = localStorage.getItem("theme") || defaults.defaultTheme;
if (theme && !document.body.dataset.theme) {
document.body.dataset.theme = theme;
}
}, []); }, []);
return ( return (
<html lang="en"> <html lang="en">
@@ -28,11 +31,9 @@ export default function RootLayout({
<Toaster /> <Toaster />
<AuthUserProvider> <AuthUserProvider>
<div className="flex flex-col min-h-screen bg-base-100"> <div className="flex flex-col min-h-screen bg-base-100">
<div className="sticky top-0 z-30 flex justify-center flex-none w-full h-16 transition-all duration-100 bg-opacity-90 text-primary-content backdrop-blur"> <NavBar />
<NavBar /> <div className="items-end grow place-items-center bg-base-200 text-primary-content">
</div> <main className=" text-base-content">{children}</main>
<div className="-mt-[4rem] grow place-items-center items-end bg-gradient-to-br from-primary to-secondary pt-20 text-primary-content ">
<main className="text-base-content">{children}</main>
</div> </div>
</div> </div>
</AuthUserProvider> </AuthUserProvider>

View File

@@ -1,6 +1,8 @@
import React from "react"; import React from "react";
import { useState } from "react";
const Loading = () => { const Loading = () => {
return ( return (
<div role="status"> <div role="status">
<svg <svg

View File

@@ -2,12 +2,11 @@ import React from "react";
import HomePageComponent from "@/components/pages/home"; import HomePageComponent from "@/components/pages/home";
const getData = async () => { const getData = async () => {
// const res = await fetch(
// `${process.env.NEXT_PUBLIC_API_URL}/api/shows/upcoming`
// );
const res = await fetch( const res = await fetch(
"https://otherway.dev.fergl.ie:3000/api/shows/upcoming" `${process.env.NEXT_PUBLIC_API_URL}/api/shows/upcoming`,
{ cache: "no-store" }
); );
return await res.json(); return await res.json();
}; };

View File

@@ -4,14 +4,8 @@ import Link from "next/link";
import { useAuthUserContext } from "@/lib/auth/authUserContext"; import { useAuthUserContext } from "@/lib/auth/authUserContext";
import Image from "next/image"; import Image from "next/image";
import { LogIn, LogOut, PlusSquare, Menu, User } from "react-feather"; import { LogIn, LogOut, PlusSquare, Menu, User } from "react-feather";
import dynamic from "next/dynamic"; import { ThemeSelector } from "../widgets/ui/themes";
const ThemeToggle = dynamic(
() => import("@/components/widgets/ui/theme/ThemeToggle"),
{
ssr: false,
}
);
const Navbar = () => { const Navbar = () => {
const { profile, loading, logOut } = useAuthUserContext(); const { profile, loading, logOut } = useAuthUserContext();
const NavMenu = profile ? ( const NavMenu = profile ? (
@@ -64,14 +58,14 @@ const Navbar = () => {
); );
return ( return (
<nav className="w-full mb-2 navbar"> <nav className="w-full navbar bg-secondary-content text-accent-focus">
<Link href="/"> <Link href="/">
<Image src="/logo.png" alt="Otherway" width={48} height={48} /> <Image src="/logo.png" alt="Otherway" width={42} height={42} />
</Link> </Link>
<div className="flex-col hidden ml-auto text-sm text-center font-body md:flex md:flex-row md:space-x-10"> <div className="flex-col hidden ml-auto text-sm text-center font-body md:flex md:flex-row md:space-x-10">
{!loading && NavMenu} {!loading && NavMenu}
</div> </div>
<ThemeToggle /> <ThemeSelector />
<div className="ml-auto lg:hidden"> <div className="ml-auto lg:hidden">
<div className="dropdown-end dropdown" data-cy="dropdown"> <div className="dropdown-end dropdown" data-cy="dropdown">

View File

@@ -6,6 +6,9 @@ import db, { users } from "@/lib/db";
import { debug } from "console"; import { debug } from "console";
import { doc, setDoc } from "firebase/firestore"; import { doc, setDoc } from "firebase/firestore";
import React from "react"; import React from "react";
import ToastService from "@/components/widgets/toast";
import logger from "@/lib/util/logging";
const ProfilePageComponentProfile = () => { const ProfilePageComponentProfile = () => {
const { loading, profile } = useAuthUserContext(); const { loading, profile } = useAuthUserContext();
const [sendReminders, setSendReminders] = React.useState(false); const [sendReminders, setSendReminders] = React.useState(false);
@@ -25,20 +28,26 @@ const ProfilePageComponentProfile = () => {
}, [profile]); }, [profile]);
const _submitProfileForm = async ($event: React.SyntheticEvent) => { const _submitProfileForm = async ($event: React.SyntheticEvent) => {
$event.preventDefault(); $event.preventDefault();
const result = await setDoc( try {
doc(users, profile?.id), const result = await setDoc(
Object.assign( doc(users, profile?.id),
{}, Object.assign(
{ {},
email, {
displayName, email,
about: about || "", displayName,
lastSeen: new Date(), about: about || "",
} lastSeen: new Date()
), }
{ merge: true } ),
); { merge: true }
console.log("ProfilePageComponentProfile", "_submitProfileForm", result); );
console.log("ProfilePageComponentProfile", "_submitProfileForm", result);
ToastService.success("Successfully updated your profile", "Success");
} catch (err) {
logger.error("ProfilePageComponentProfile", "_submitProfileForm", err);
ToastService.error("Failed to update your profile.");
}
}; };
return ( return (
<form className="space-y-8 divide-y" onSubmit={_submitProfileForm}> <form className="space-y-8 divide-y" onSubmit={_submitProfileForm}>
@@ -106,10 +115,6 @@ const ProfilePageComponentProfile = () => {
updateFormValue={(v) => setAbout(v)} updateFormValue={(v) => setAbout(v)}
showLabel={false} showLabel={false}
/> />
<p className="mt-2 text-sm ">
Write a few sentences about yourself.
</p>
</div> </div>
</div> </div>
@@ -126,7 +131,8 @@ const ProfilePageComponentProfile = () => {
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" /> <path
d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg> </svg>
</span> </span>
<button <button

View File

@@ -3,29 +3,29 @@ import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
import { Show } from "@/models"; import { Show } from "@/models";
import React from "react"; import React from "react";
import { MdAddAlarm } from "react-icons/md"; import { MdAddAlarm } from "react-icons/md";
import { error, success, warning } from "./toast/toastService"; import ToastService from "./toast/toastService";
const RemindMeButton = ({ showId }: { showId: string }) => { const RemindMeButton = ({ showId }: { showId: string }) => {
const { profile } = useFirebaseAuth(); const { profile } = useFirebaseAuth();
const createShowReminder = async () => { const createShowReminder = async () => {
if (profile?.id) { if (profile?.id) {
var response = await fetch( const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/reminders`, `${process.env.NEXT_PUBLIC_API_URL}/api/reminders`,
{ {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
}, },
body: JSON.stringify({ body: JSON.stringify({
userId: profile?.id, userId: profile?.id,
showId: profile?.id, showId: profile?.id
}), })
} }
); );
if (response.status === 201) { if (response.status === 201) {
success("Reminder created successfully"); ToastService.success("Reminder created successfully");
} else { } else {
error("Unable to create reminder at this time"); ToastService.error("Unable to create reminder at this time");
} }
} }
}; };

View File

@@ -1,3 +1,3 @@
import { error, success, warning } from "./toastService"; import ToastService from "./toastService";
export { success, warning, error }; export default ToastService;

View File

@@ -4,12 +4,12 @@ import ToastComponent, { ToastType } from "./ToastComponent";
import { import {
RiAlarmWarningLine, RiAlarmWarningLine,
RiErrorWarningLine, RiErrorWarningLine,
RiShieldCheckLine RiShieldCheckLine,
} from "react-icons/ri"; } from "react-icons/ri";
const success = (message: string, title?: string) => { const ToastService = {
toast.custom( success: (message: string, title?: string) => {
(t) => ( toast.custom((t) => (
<ToastComponent <ToastComponent
title={title ?? "Success"} title={title ?? "Success"}
body={message} body={message}
@@ -19,12 +19,10 @@ const success = (message: string, title?: string) => {
toast.dismiss(t.id); toast.dismiss(t.id);
}} }}
/> />
) ));
); },
}; warning: (message: string, title?: string) => {
const warning = (message: string, title?: string) => { toast.custom((t) => (
toast.custom(
(t) => (
<ToastComponent <ToastComponent
title={title ?? "Warning"} title={title ?? "Warning"}
body={message} body={message}
@@ -34,12 +32,10 @@ const warning = (message: string, title?: string) => {
toast.dismiss(t.id); toast.dismiss(t.id);
}} }}
/> />
) ));
); },
}; error: (message: string, title?: string) => {
const error = (message: string, title?: string) => { toast.custom((t) => (
toast.custom(
(t) => (
<ToastComponent <ToastComponent
title={title ?? "Error"} title={title ?? "Error"}
body={message} body={message}
@@ -49,7 +45,7 @@ const error = (message: string, title?: string) => {
toast.dismiss(t.id); toast.dismiss(t.id);
}} }}
/> />
) ));
); },
}; };
export { success, warning, error }; export default ToastService;

View File

@@ -0,0 +1,83 @@
"use client";
import React from "react";
import { themes } from "./themes";
import { useState, useEffect } from "react";
import { IoColorPaletteOutline } from "react-icons/io5";
import { BiDownArrow } from "react-icons/bi";
import { defaults } from "@/lib/constants";
const ThemeSelector = () => {
const [activeTheme, setActiveTheme] = useState(
document?.body.dataset.theme ||
localStorage.getItem("theme") ||
defaults.defaultTheme
);
useEffect(() => {
if (document) {
document.body.dataset.theme = activeTheme;
window.localStorage.setItem("theme", activeTheme);
}
}, [activeTheme]);
const _switchTheme = (theme: string) => {
setActiveTheme(theme);
const elem = document.activeElement as HTMLElement;
elem?.blur();
};
return (
<div title="Change Theme" className="dropdown-end dropdown">
<div tabIndex={0} className="gap-1 normal-case btn-ghost btn">
<IoColorPaletteOutline className="inline-block w-5 h-5 stroke-current md:h-6 md:w-6" />
<span className="hidden md:inline">Theme</span>
<BiDownArrow className="hidden w-3 h-3 ml-1 fill-current opacity-60 sm:inline-block" />
</div>
<div
className="dropdown-content rounded-t-box rounded-b-box top-px h-[70vh] max-h-96 w-52 overflow-y-auto bg-base-200 text-base-content shadow-2xl">
<div className="grid grid-cols-1 gap-3 p-3" tabIndex={0}>
{themes.map((theme) => (
<button
key={theme.id}
className="overflow-hidden text-left rounded-lg outline-base-content"
onClick={() => _switchTheme(theme.id)}
>
<div
data-theme={theme.id}
className="w-full font-sans cursor-pointer bg-base-100 text-base-content"
>
<div className="grid grid-cols-5 grid-rows-3">
<div className="flex items-center col-span-5 row-span-3 row-start-1 gap-2 px-4 py-3">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
className="invisible w-3 h-3"
>
<path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z" />
</svg>
<div className="flex-grow text-sm font-bold">
{theme.id}
</div>
<div className="flex flex-wrap flex-shrink-0 h-full gap-1">
<div className="w-2 rounded bg-primary">
<div className="w-2 rounded bg-secondary">
<div className="w-2 rounded bg-accent">
<div className="w-2 rounded bg-neutral"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</button>
))}
</div>
</div>
</div>
);
};
export default ThemeSelector;

View File

@@ -0,0 +1,4 @@
import ThemeSelector from "./ThemeSelector";
import ThemeToggle from "./ThemeToggle";
export { ThemeSelector, ThemeToggle };

View File

@@ -0,0 +1,118 @@
export let themes = [
{
name: "🌝 light",
id: "light",
},
{
name: "🌚 dark",
id: "dark",
},
{
name: "🧁 cupcake",
id: "cupcake",
},
{
name: "🐝 bumblebee",
id: "bumblebee",
},
{
name: "✳️ Emerald",
id: "emerald",
},
{
name: "🏢 Corporate",
id: "corporate",
},
{
name: "🌃 synthwave",
id: "synthwave",
},
{
name: "👴 retro",
id: "retro",
},
{
name: "🤖 cyberpunk",
id: "cyberpunk",
},
{
name: "🌸 valentine",
id: "valentine",
},
{
name: "🎃 halloween",
id: "halloween",
},
{
name: "🌷 garden",
id: "garden",
},
{
name: "🌲 forest",
id: "forest",
},
{
name: "🐟 aqua",
id: "aqua",
},
{
name: "👓 lofi",
id: "lofi",
},
{
name: "🖍 pastel",
id: "pastel",
},
{
name: "🧚‍♀️ fantasy",
id: "fantasy",
},
{
name: "📝 Wireframe",
id: "wireframe",
},
{
name: "🏴 black",
id: "black",
},
{
name: "💎 luxury",
id: "luxury",
},
{
name: "🧛‍♂️ dracula",
id: "dracula",
},
{
name: "🖨 CMYK",
id: "cmyk",
},
{
name: "🍁 Autumn",
id: "autumn",
},
{
name: "💼 Business",
id: "business",
},
{
name: "💊 Acid",
id: "acid",
},
{
name: "🍋 Lemonade",
id: "lemonade",
},
{
name: "🌙 Night",
id: "night",
},
{
name: "☕️ Coffee",
id: "coffee",
},
{
name: "❄️ Winter",
id: "winter",
},
];

View File

@@ -11,13 +11,14 @@ import {
signInWithPopup, signInWithPopup,
signOut, signOut,
TwitterAuthProvider, TwitterAuthProvider,
UserCredential, UserCredential
} from "firebase/auth"; } from "firebase/auth";
import { app } from "./firebase"; import { app } from "./firebase";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Profile } from "@/models"; import { Profile } from "@/models";
import { users } from "../db"; import { users } from "../db";
import { doc, setDoc } from "firebase/firestore"; import { doc, getDoc, setDoc } from "firebase/firestore";
import { servicesVersion } from "@ts-morph/common/lib/typescript";
export default function useFirebaseAuth() { export default function useFirebaseAuth() {
const [profile, setProfile] = useState<Profile | undefined>(); const [profile, setProfile] = useState<Profile | undefined>();
@@ -26,30 +27,30 @@ export default function useFirebaseAuth() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const getUserProfile = useCallback(() => { const getUserProfile = useCallback(async () => {
if (auth.currentUser !== null) { if (auth.currentUser !== null) {
// The user object has basic properties such as display name, email, etc. // The user object has basic properties such as display name, email, etc.
// Going forward we may look this up from the user table in firestore // Going forward we may look this up from the user table in firestore
const profile = new Profile( const savedProfileRef = await getDoc(doc(users, auth.currentUser.uid));
const savedProfile = savedProfileRef.data();
const profile: Profile = new Profile(
auth.currentUser.uid, auth.currentUser.uid,
auth.currentUser.email, (savedProfile?.email || auth.currentUser.email) as string,
auth.currentUser.displayName, (savedProfile?.displayName || auth.currentUser.email) as string,
auth.currentUser.photoURL, (savedProfile?.photoURL || auth.currentUser.email) as string,
auth.currentUser.emailVerified, savedProfile?.about as string
new Date()
); );
setProfile(profile); setProfile(profile);
setDoc(doc(users, auth.currentUser.uid), Object.assign({}, profile), { await setDoc(doc(users, auth.currentUser.uid), Object.assign({}, profile), {
merge: true, merge: true
}); });
return profile; return profile;
} }
}, [auth.currentUser]); }, [auth.currentUser]);
const authStateChanged = useCallback( const authStateChanged = useCallback(async (user: any) => {
(user: any) => {
if (user) { if (user) {
setLoading(true); setLoading(true);
const profile = getUserProfile(); const profile = await getUserProfile();
setProfile(profile); setProfile(profile);
} }
setLoading(false); setLoading(false);
@@ -108,7 +109,7 @@ export default function useFirebaseAuth() {
const result = await _processSignIn(provider); const result = await _processSignIn(provider);
if (result) { if (result) {
const credential = GoogleAuthProvider.credentialFromResult(result); const credential = GoogleAuthProvider.credentialFromResult(result);
const profile = getUserProfile(); const profile = await getUserProfile();
setProfile(profile); setProfile(profile);
router.push("/"); router.push("/");
} }
@@ -118,7 +119,7 @@ export default function useFirebaseAuth() {
const result = await _processSignIn(provider); const result = await _processSignIn(provider);
if (result) { if (result) {
const credential = TwitterAuthProvider.credentialFromResult(result); const credential = TwitterAuthProvider.credentialFromResult(result);
const profile = getUserProfile(); const profile = await getUserProfile();
setProfile(profile); setProfile(profile);
router.push("/"); router.push("/");
} }
@@ -142,6 +143,6 @@ export default function useFirebaseAuth() {
signInWithGoogle, signInWithGoogle,
signInWithTwitter, signInWithTwitter,
signInWithFacebook, signInWithFacebook,
getUserProfile, getUserProfile
}; };
} }

View File

@@ -7,7 +7,7 @@ import {
WithFieldValue, WithFieldValue,
QueryDocumentSnapshot, QueryDocumentSnapshot,
SnapshotOptions, SnapshotOptions,
Timestamp, Timestamp
} from "firebase/firestore"; } from "firebase/firestore";
const firebaseConfig = { const firebaseConfig = {
@@ -17,7 +17,7 @@ const firebaseConfig = {
storageBucket: "radio-otherway.appspot.com", storageBucket: "radio-otherway.appspot.com",
messagingSenderId: "47147490249", messagingSenderId: "47147490249",
appId: "1:47147490249:web:a84515b3ce1c481826e618", appId: "1:47147490249:web:a84515b3ce1c481826e618",
measurementId: "G-12YB78EZM4", measurementId: "G-12YB78EZM4"
}; };
export const firebaseApp = initializeApp(firebaseConfig); export const firebaseApp = initializeApp(firebaseConfig);
const firestore = getFirestore(); const firestore = getFirestore();
@@ -30,7 +30,7 @@ const showConverter = {
...show, ...show,
date: show.date date: show.date
? Timestamp.fromDate(new Date(show.date as string)) ? Timestamp.fromDate(new Date(show.date as string))
: new Date(), : new Date()
}; };
}, },
fromFirestore( fromFirestore(
@@ -39,7 +39,7 @@ const showConverter = {
): Show { ): Show {
const data = snapshot.data(options)!; const data = snapshot.data(options)!;
return new Show(snapshot.id, data.title, data.date.toDate(), data.creator); return new Show(snapshot.id, data.title, data.date.toDate(), data.creator);
}, }
}; };
// Import all your model types // Import all your model types
@@ -52,4 +52,7 @@ export const shows =
export const reminders = createCollection<Reminder>("reminders"); export const reminders = createCollection<Reminder>("reminders");
export const remindersProcessed = export const remindersProcessed =
createCollection<RemindersProcessed>("reminders"); createCollection<RemindersProcessed>("reminders");
export default firestore; export default firestore;
export { createCollection };

26
src/lib/db/settings.ts Normal file
View File

@@ -0,0 +1,26 @@
import { Show } from "@/models";
import { createCollection } from "@/lib/db/index";
import { doc, query, setDoc, where } from "@firebase/firestore";
import { getDoc } from "firebase/firestore";
interface Setting {
value: string;
updated: Date;
}
const settingsCollection = createCollection<Setting>("settings");
const Settings = {
read: async (key: string): Promise<string | undefined> => {
const value = (await getDoc(doc(settingsCollection, key))).data();
return value?.value || undefined;
},
write: async (key: string, value: string) => {
await setDoc(doc(settingsCollection, key), {
value,
updated: new Date()
});
}
};
export default Settings;

View File

@@ -0,0 +1,22 @@
const { google } = require("googleapis");
const GOOGLE_PRIVATE_KEY = process.env.GOOGLE_CALENDAR_CREDENTIALS_PRIVATE_KEY;
const GOOGLE_CLIENT_EMAIL =
process.env.GOOGLE_CALENDAR_CREDENTIALS_CLIENT_EMAIL;
const GOOGLE_PROJECT_NUMBER = process.env.GOOGLE_CALENDAR_PROJECT_ID;
const GOOGLE_CALENDAR_ID = process.env.GOOGLE_CALENDAR_ID;
const SCOPES = ["https://www.googleapis.com/auth/calendar"];
const jwtClient = new google.auth.JWT(
GOOGLE_CLIENT_EMAIL,
null,
GOOGLE_PRIVATE_KEY,
SCOPES
);
const calendar = google.calendar({
version: "v3",
project: GOOGLE_PROJECT_NUMBER,
auth: jwtClient
});
export default calendar;
export {GOOGLE_CALENDAR_ID}

View File

@@ -1,44 +1,31 @@
import { Show } from "@/models"; import calendar, { GOOGLE_CALENDAR_ID } from "@/lib/util/google/calendar";
import logger from "../logging";
const { google } = require("googleapis"); const getCalendarEntries = async (syncToken?: string) => {
const GOOGLE_PRIVATE_KEY = process.env.GOOGLE_CALENDAR_CREDENTIALS_PRIVATE_KEY;
const GOOGLE_CLIENT_EMAIL =
process.env.GOOGLE_CALENDAR_CREDENTIALS_CLIENT_EMAIL;
const GOOGLE_PROJECT_NUMBER = process.env.GOOGLE_CALENDAR_PROJECT_ID;
const GOOGLE_CALENDAR_ID = process.env.GOOGLE_CALENDAR_ID;
const SCOPES = ["https://www.googleapis.com/auth/calendar"];
const jwtClient = new google.auth.JWT(
GOOGLE_CLIENT_EMAIL,
null,
GOOGLE_PRIVATE_KEY,
SCOPES
);
const calendar = google.calendar({
version: "v3",
project: GOOGLE_PROJECT_NUMBER,
auth: jwtClient
});
const getCalendarEntries = async () => {
try { try {
const events = await calendar.events.list({ const e = await calendar.events.list({
calendarId: GOOGLE_CALENDAR_ID, calendarId: GOOGLE_CALENDAR_ID,
timeMin: new Date().toISOString(),
maxResults: 10, maxResults: 10,
singleEvents: true, singleEvents: true,
orderBy: "startTime" syncToken: syncToken
});
return events.data.items.map((r: any) => {
return {
id: r.id,
title: r.summary,
date: r.start.dateTime,
creator: r.creator.email
};
}); });
const events = _mapEvents(e);
return events;
} catch (err) { } catch (err) {
logger.error("calendarReader", "Unable to read events", err);
} }
return null; return null;
}; };
const _mapEvents = (events: any) => {
const mapped = events.data.items.map((r: any) => ({
id: r.id,
title: r.summary,
date: r.start.dateTime,
creator: r.creator.email
}));
return {
syncToken: events.data.nextSyncToken,
events: mapped
};
};
export { getCalendarEntries }; export { getCalendarEntries };

View File

@@ -0,0 +1,14 @@
import calendar, { GOOGLE_CALENDAR_ID } from "@/lib/util/google/calendar";
import { uuidv4 } from "@firebase/util";
const setupCalendarWebhook = async () => {
calendar.events.watch({
resource: {
id: uuidv4(),
type: "web_hook",
address: `https://external.fergl.ie/api/shows/calendar`
},
calendarId: GOOGLE_CALENDAR_ID
});
};
export { setupCalendarWebhook };

View File

@@ -3,22 +3,23 @@ export default class Profile {
email: string | null; email: string | null;
displayName: string | null; displayName: string | null;
photoURL: string | null; photoURL: string | null;
emailVerified: boolean; emailVerified: boolean = false;
about?: String; about?: String;
lastSeen: Date; lastSeen: Date;
constructor( constructor(
id: string, id: string,
email: string | null, email: string | null,
displayName: string | null, displayName: string | null,
photoURL: string | null, photoURL: string | null,
emailVerified: boolean, about?: string,
lastSeen?: Date lastSeen?: Date
) { ) {
this.id = id; this.id = id;
this.email = email; this.email = email;
this.displayName = displayName; this.displayName = displayName;
this.photoURL = photoURL; this.photoURL = photoURL;
this.emailVerified = emailVerified; this.about = about || "";
this.lastSeen = lastSeen || new Date(); this.lastSeen = lastSeen || new Date();
} }

View File

@@ -4,25 +4,34 @@ import logger from "@/lib/util/logging";
import { doc, setDoc } from "@firebase/firestore"; import { doc, setDoc } from "@firebase/firestore";
import { shows } from "@/lib/db"; import { shows } from "@/lib/db";
import { Show } from "@/models"; import { Show } from "@/models";
import Settings from "@/lib/db/settings";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
try { try {
const e = await getCalendarEntries(); const syncToken = await Settings.read("CalendarSyncToken");
const entries = e.map((r: any) => Show.fromJson(r)); const e = await getCalendarEntries(syncToken);
for (const entry of entries) { if (!e?.events) {
logger.debug("Storing show", entry); res.status(204).json({ result: "No calendar entries found" });
const showRef = doc(shows, entry.id); } else {
await setDoc(showRef, { const entries = e?.events.map((r: any) => Show.fromJson(r));
title: entry.title, for (const entry of entries) {
date: entry.date, logger.debug("Storing show", entry);
creator: entry.creator const showRef = doc(shows, entry.id);
}, { merge: true }); await setDoc(showRef, {
title: entry.title,
date: entry.date,
creator: entry.creator
}, { merge: true });
}
logger.debug("Stored show", res);
if (e?.syncToken) {
await Settings.write("CalendarSyncToken", e?.syncToken);
}
res.status(200).json({ status: "OK", entries });
} }
logger.debug("Stored show", res);
res.status(200).json({ status: "OK", entries });
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
res.status(500).json({status: "Error"}); res.status(500).json({ status: "Error" });
} }
res.end(); res.end();
}; };

View File

@@ -1,6 +1,8 @@
import {NextApiRequest, NextApiResponse} from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { setupCalendarWebhook } from "@/lib/util/google/calendarWatcher";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json({hello: process.env}); await setupCalendarWebhook();
res.status(200).json({ hello: process.env });
}; };
export default handler; export default handler;

View File

@@ -0,0 +1,33 @@
import { NextApiRequest, NextApiResponse } from "next";
import { doc, getDocs, query, setDoc, where } from "@firebase/firestore";
import { shows } from "@/lib/db";
import logger from "@/lib/util/logging";
import { Show } from "@/models";
import { getCalendarEntries } from "@/lib/util/google/calendarReader";
import Settings from "@/lib/db/settings";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const resourceId = req.headers["x-goog-resource-id"];
const channelToken = req.headers["x-goog-channel-token"];
const channelId = req.headers["x-goog-channel-id"];
const resourceState = req.headers["x-goog-resource-state"];
logger.debug("Webhook callback",
resourceId,
channelToken,
channelId,
resourceState
);
const changed = await getCalendarEntries();
const entries = changed?.events.map((r: any) => Show.fromJson(r));
for (const entry of entries) {
const showRef = doc(shows, entry.id);
await setDoc(showRef, {
title: entry.title,
date: entry.date,
creator: entry.creator
}, { merge: true });
}
res.status(200).json({ result: "We got pinged" });
res.end();
};

View File

@@ -830,47 +830,47 @@
"@types/node" "*" "@types/node" "*"
"@typescript-eslint/parser@^5.42.0": "@typescript-eslint/parser@^5.42.0":
version "5.53.0" version "5.54.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.53.0.tgz#a1f2b9ae73b83181098747e96683f1b249ecab52" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.54.0.tgz#def186eb1b1dbd0439df0dacc44fb6d8d5c417fe"
integrity sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ== integrity sha512-aAVL3Mu2qTi+h/r04WI/5PfNWvO6pdhpeMRWk9R7rEV4mwJNzoWf5CCU5vDKBsPIFQFjEq1xg7XBI2rjiMXQbQ==
dependencies: dependencies:
"@typescript-eslint/scope-manager" "5.53.0" "@typescript-eslint/scope-manager" "5.54.0"
"@typescript-eslint/types" "5.53.0" "@typescript-eslint/types" "5.54.0"
"@typescript-eslint/typescript-estree" "5.53.0" "@typescript-eslint/typescript-estree" "5.54.0"
debug "^4.3.4" debug "^4.3.4"
"@typescript-eslint/scope-manager@5.53.0": "@typescript-eslint/scope-manager@5.54.0":
version "5.53.0" version "5.54.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz#42b54f280e33c82939275a42649701024f3fafef" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.54.0.tgz#74b28ac9a3fc8166f04e806c957adb8c1fd00536"
integrity sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w== integrity sha512-VTPYNZ7vaWtYna9M4oD42zENOBrb+ZYyCNdFs949GcN8Miwn37b8b7eMj+EZaq7VK9fx0Jd+JhmkhjFhvnovhg==
dependencies: dependencies:
"@typescript-eslint/types" "5.53.0" "@typescript-eslint/types" "5.54.0"
"@typescript-eslint/visitor-keys" "5.53.0" "@typescript-eslint/visitor-keys" "5.54.0"
"@typescript-eslint/types@5.53.0": "@typescript-eslint/types@5.54.0":
version "5.53.0" version "5.54.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.53.0.tgz#f79eca62b97e518ee124086a21a24f3be267026f" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.54.0.tgz#7d519df01f50739254d89378e0dcac504cab2740"
integrity sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A== integrity sha512-nExy+fDCBEgqblasfeE3aQ3NuafBUxZxgxXcYfzYRZFHdVvk5q60KhCSkG0noHgHRo/xQ/BOzURLZAafFpTkmQ==
"@typescript-eslint/typescript-estree@5.53.0": "@typescript-eslint/typescript-estree@5.54.0":
version "5.53.0" version "5.54.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz#bc651dc28cf18ab248ecd18a4c886c744aebd690" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.54.0.tgz#f6f3440cabee8a43a0b25fa498213ebb61fdfe99"
integrity sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w== integrity sha512-X2rJG97Wj/VRo5YxJ8Qx26Zqf0RRKsVHd4sav8NElhbZzhpBI8jU54i6hfo9eheumj4oO4dcRN1B/zIVEqR/MQ==
dependencies: dependencies:
"@typescript-eslint/types" "5.53.0" "@typescript-eslint/types" "5.54.0"
"@typescript-eslint/visitor-keys" "5.53.0" "@typescript-eslint/visitor-keys" "5.54.0"
debug "^4.3.4" debug "^4.3.4"
globby "^11.1.0" globby "^11.1.0"
is-glob "^4.0.3" is-glob "^4.0.3"
semver "^7.3.7" semver "^7.3.7"
tsutils "^3.21.0" tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@5.53.0": "@typescript-eslint/visitor-keys@5.54.0":
version "5.53.0" version "5.54.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz#8a5126623937cdd909c30d8fa72f79fa56cc1a9f" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.54.0.tgz#846878afbf0cd67c19cfa8d75947383d4490db8f"
integrity sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w== integrity sha512-xu4wT7aRCakGINTLGeyGqDn+78BwFlggwBjnHa1ar/KaGagnmwLYmlrXIrgAaQ3AE1Vd6nLfKASm7LrFHNbKGA==
dependencies: dependencies:
"@typescript-eslint/types" "5.53.0" "@typescript-eslint/types" "5.54.0"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@upstash/qstash@^0.3.6": "@upstash/qstash@^0.3.6":
@@ -1409,9 +1409,9 @@ csstype@^3.0.2:
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
daisyui@^2.49.0: daisyui@^2.49.0:
version "2.51.1" version "2.51.2"
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-2.51.1.tgz#20fe01f3f3caa8628ebb30fcc1b99341183d1702" resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-2.51.2.tgz#831440e02ea55f773f519a69b7f746824f4f5f84"
integrity sha512-rMtdB8Rh8Ghd3+FDlMe63Caw+IopKIf7tviJGEx8gkjj5H+RCvU1bEbjx6DHsMsu477Gv5aafrLIdoUL+iMRrw== integrity sha512-Jnfknn9HrOBNzj1kUI9g/kqNSA5oP5B/r6X4Ff/cdMO27glisO2Oi9QAO8hKwMN9LzfWGdfyqvJGvc3Ji78CXA==
dependencies: dependencies:
color "^4.2" color "^4.2"
css-selector-tokenizer "^0.8.0" css-selector-tokenizer "^0.8.0"
@@ -1573,9 +1573,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.4.284: electron-to-chromium@^1.4.284:
version "1.4.311" version "1.4.312"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.311.tgz#953bc9a4767f5ce8ec125f9a1ad8e00e8f67e479" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.312.tgz#e70a5b46252814ffc576b2c29032e1a559b9ad53"
integrity sha512-RoDlZufvrtr2Nx3Yx5MB8jX3aHIxm8nRWPJm3yVvyHmyKaRvn90RjzB6hNnt0AkhS3IInJdyRfQb4mWhPvUjVw== integrity sha512-e7g+PzxzkbiCD1aNhdj+Tx3TLlfrQF/Lf+LAaUdoLvB1kCxf9wJimqXdWEqnoiYjFtxIR1hGBmoHsBIcCBNOMA==
emoji-regex@^8.0.0: emoji-regex@^8.0.0:
version "8.0.0" version "8.0.0"