Skip to content

Commit

Permalink
feat: support group address
Browse files Browse the repository at this point in the history
  • Loading branch information
yamitzky committed Feb 13, 2025
1 parent 6a1384f commit 9657ff7
Show file tree
Hide file tree
Showing 13 changed files with 205 additions and 23 deletions.
9 changes: 6 additions & 3 deletions apps/web/app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type { MetaFunction } from '@remix-run/node'
import { type LoaderFunctionArgs, json } from '@remix-run/node'
import { useLoaderData, useNavigate } from '@remix-run/react'
import { config } from '@synk-cal/core'
import { getEvents } from '@synk-cal/usecase'
import { addDays, format, parseISO, startOfWeek, subDays } from 'date-fns'
import { Calendar } from '~/components/Calendar'
import { getAuthRepository } from '~/services/getAuthRepository'
import { getCalendarRepository } from '~/services/getCalendarRepository'
import { getGroupRepository } from '~/services/getGRoupRepository'

export const meta: MetaFunction = () => {
return [{ title: 'Synk Calendar' }, { name: 'description', content: 'Calendar viewer' }]
Expand Down Expand Up @@ -35,14 +37,15 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const minDate = subDays(startDate, 7)
const maxDate = addDays(endDate, 7)

const repositories = config.CALENDAR_IDS.map((id) => ({
const calendarRepositories = config.CALENDAR_IDS.map((id) => ({
id,
repository: getCalendarRepository(id),
}))
const groupRepository = getGroupRepository()
const calendars = await Promise.all(
repositories.map(async ({ id, repository }) => ({
calendarRepositories.map(async ({ id, repository }) => ({
calendarId: id,
events: await repository.getEvents(minDate, maxDate),
events: await getEvents({ calendarRepository: repository, groupRepository, minDate, maxDate }),
})),
)
return json({
Expand Down
10 changes: 9 additions & 1 deletion apps/web/app/routes/process_reminder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { json } from '@remix-run/node'
import { config } from '@synk-cal/core'
import { processReminders } from '@synk-cal/usecase'
import { getCalendarRepository } from '~/services/getCalendarRepository'
import { getGroupRepository } from '~/services/getGroupRepository'
import { getNotificationRepositories } from '~/services/getNotificationRepository'
import { getReminderSettingsRepository } from '~/services/getReminderSettingsRepository'

Expand Down Expand Up @@ -30,11 +31,18 @@ export const action: ActionFunction = async ({ request }) => {
}

const calendarRepositories = config.CALENDAR_IDS.map((id) => getCalendarRepository(id))
const groupRepository = getGroupRepository()
const notificationRepositories = getNotificationRepositories()
const reminderSettingsRepository = getReminderSettingsRepository()

try {
await processReminders(baseTime, calendarRepositories, notificationRepositories, reminderSettingsRepository)
await processReminders({
baseTime,
groupRepository,
calendarRepositories,
notificationRepositories,
reminderSettingsRepository,
})
return json({ success: true })
} catch (error) {
console.error('Error processing reminders:', error)
Expand Down
15 changes: 4 additions & 11 deletions apps/web/app/routes/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from '@remix-run/node'
import { useFetcher, useLoaderData } from '@remix-run/react'
import { ReminderSetting, config } from '@synk-cal/core'
import { ReminderSetting } from '@synk-cal/core'
import { ReminderSettings } from '~/components/Settings'
import { getAuthRepository } from '~/services/getAuthRepository'
import { getReminderSettingsRepository } from '~/services/getReminderSettingsRepository'

const NOTIFY_BEFORE_OPTIONS = config.REMINDER_MINUTES_BEFORE_OPTIONS

export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await getAuthRepository()?.getUserFromHeader(request.headers)

Expand All @@ -21,7 +19,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
user,
isReminderSettingsEnabled: !!reminderSettingsRepo,
reminders,
notifyBeforeOptions: NOTIFY_BEFORE_OPTIONS,
})
}

Expand Down Expand Up @@ -55,7 +52,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
}

export default function SettingsRoute() {
const { user, reminders, isReminderSettingsEnabled, notifyBeforeOptions } = useLoaderData<typeof loader>()
const { user, reminders, isReminderSettingsEnabled } = useLoaderData<typeof loader>()
const fetcher = useFetcher()

const handleRemindersChange = (newReminders: ReminderSetting[]) => {
Expand All @@ -75,12 +72,8 @@ export default function SettingsRoute() {
) : !isReminderSettingsEnabled ? (
<div>Reminder settings are not enabled.</div>
) : (
<ReminderSettings
user={user}
reminders={currentReminders}
minutesBeforeOptions={notifyBeforeOptions}
onChange={handleRemindersChange}
/>
// FIXME: reminder options config
<ReminderSettings user={user} reminders={currentReminders} onChange={handleRemindersChange} />
)}
</div>
)
Expand Down
8 changes: 8 additions & 0 deletions apps/web/app/services/getGroupRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { GroupRepository, config } from '@synk-cal/core'
import { GoogleGroupRepository } from '@synk-cal/google'

export function getGroupRepository(): GroupRepository | undefined {
if (config.GROUP_PROVIDER === 'google') {
return new GoogleGroupRepository()
}
}
2 changes: 1 addition & 1 deletion packages/core/src/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type CalendarEvent = {
name: string
url: string
}
people?: Array<{
people: Array<{
email?: string
displayName?: string
responseStatus?: ResponseStatus
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const ConfigSchema = v.object({
REMINDER_SETTINGS_FIRESTORE_DATABASE_ID: v.optional(v.string()),
AUTH_PROVIDER: v.optional(v.union([v.literal('google-iap')])),
WEBHOOK_URL: v.optional(v.pipe(v.string(), v.url())),
GROUP_PROVIDER: v.optional(v.union([v.literal('google')])),
GROUP_CUSTOMER_ID: v.optional(v.string()),
})

export type Config = v.InferOutput<typeof ConfigSchema>
Expand All @@ -56,6 +58,8 @@ export function parseConfig(env: NodeJS.ProcessEnv): Config {
REMINDER_TEMPLATE: env.REMINDER_TEMPLATE,
AUTH_PROVIDER: env.AUTH_PROVIDER,
WEBHOOK_URL: env.WEBHOOK_URL,
GROUP_PROVIDER: env.GROUP_PROVIDER,
GROUP_CUSTOMER_ID: env.GROUP_CUSTOMER_ID,
})
}

Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface Group {
id: string
email: string
name: string
description?: string
}

export interface GroupMember {
id: string
email: string
type: string
}

export interface GroupRepository {
getGroups(): Promise<Group[]>
getGroupMembers(groupId: string): Promise<GroupMember[]>
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './calendar'
export { config } from './config'
export * from './group'
export * from './notification'
export * from './reminder'
export * from './user'
65 changes: 65 additions & 0 deletions packages/google/src/google_group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { type Group, type GroupMember, type GroupRepository, config } from '@synk-cal/core'
import { type cloudidentity_v1, google } from 'googleapis'

let cloudIdentityClient: cloudidentity_v1.Cloudidentity | null = null

export class GoogleGroupRepository implements GroupRepository {
async getCloudIdentityClient() {
if (cloudIdentityClient) {
return cloudIdentityClient
}

// Google Cloud Identity API setup
const auth = new google.auth.GoogleAuth({
keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS,
scopes: [
'https://www.googleapis.com/auth/cloud-identity.groups.readonly',
'https://www.googleapis.com/auth/cloud-identity.users.readonly',
],
})

const authClient = await auth.getClient()
// @ts-expect-error
cloudIdentityClient = google.cloudidentity({ version: 'v1', auth: authClient })

return cloudIdentityClient
}

async getGroups(): Promise<Group[]> {
const client = await this.getCloudIdentityClient()

const response = await client.groups.list({
parent: `customers/${config.GROUP_CUSTOMER_ID}`,
view: 'BASIC',
// FIXME: handle pagination
pageSize: 1000,
})

const groups = response.data.groups || []

return groups.map((group) => ({
id: group.name ?? '',
email: group.groupKey?.id ?? '',
name: group.displayName ?? '',
description: group.description ?? undefined,
}))
}

async getGroupMembers(groupId: string): Promise<GroupMember[]> {
const client = await this.getCloudIdentityClient()

const response = await client.groups.memberships.list({
parent: groupId,
// FIXME: handle pagination
pageSize: 1000,
})

const memberships = response.data.memberships || []

return memberships.map((membership) => ({
id: membership.name ?? '',
email: membership.preferredMemberKey?.id ?? '',
type: membership.type ?? '',
}))
}
}
1 change: 1 addition & 0 deletions packages/google/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './firestore'
export * from './google_calendar'
export * from './google_group'
export * from './iap'
67 changes: 67 additions & 0 deletions packages/usecase/src/get_events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { type CalendarEvent, type CalendarRepository, Group, GroupMember, type GroupRepository } from '@synk-cal/core'

interface GetEventsUseCaseParams {
calendarRepository: CalendarRepository
groupRepository?: GroupRepository
minDate: Date
maxDate: Date
}

export const getEvents = async ({
calendarRepository,
groupRepository,
minDate,
maxDate,
}: GetEventsUseCaseParams): Promise<CalendarEvent[]> => {
const events = await calendarRepository.getEvents(minDate, maxDate)

const expandedEvents: CalendarEvent[] = []

const groupCache: Record<string, { group: Group; members?: GroupMember[] }> = {}
if (groupRepository) {
const groups = await groupRepository.getGroups()
for (const group of groups) {
// members are fetched lazily
groupCache[group.email] = { group }
console.log(group.email)
}
}

for (const event of events) {
const expandedAttendees = []
for (const attendee of event.people) {
const cachedGroup = attendee.email ? groupCache[attendee.email] : undefined
if (cachedGroup && groupRepository) {
try {
let groupMembers: GroupMember[]
if (cachedGroup.members) {
groupMembers = cachedGroup.members
} else {
groupMembers = await groupRepository.getGroupMembers(cachedGroup.group.id)
groupCache[cachedGroup.group.email] = { ...cachedGroup, members: groupMembers }
}
for (const member of groupMembers) {
expandedAttendees.push({
email: member.email,
responseStatus: attendee.responseStatus,
organizer: false,
})
}
} catch (error) {
// fallback
console.error(`Error expanding group ${attendee.email}:`, error)
expandedAttendees.push(attendee)
}
} else {
expandedAttendees.push(attendee)
}
}

expandedEvents.push({
...event,
people: expandedAttendees,
})
}

return expandedEvents
}
1 change: 1 addition & 0 deletions packages/usecase/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './get_events'
export * from './process_reminders'
28 changes: 21 additions & 7 deletions packages/usecase/src/process_reminders.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
CalendarRepository,
GroupRepository,
NotificationRepository,
ReminderSetting,
ReminderSettingsRepository,
Expand All @@ -8,6 +9,7 @@ import { config } from '@synk-cal/core'
import { addDays, isSameMinute, parseISO, subMinutes } from 'date-fns'
import { formatInTimeZone, fromZonedTime } from 'date-fns-tz'
import { Eta } from 'eta'
import { getEvents } from './get_events'

const DEFAULT_TEMPLATE = `Reminder: "<%= it.title %>" starts <%=
it.minutesBefore !== undefined
Expand All @@ -30,18 +32,30 @@ const getReminderTime = (eventStart: Date, setting: ReminderSetting) => {
}
}

export async function processReminders(
baseTime: Date,
calendarRepositories: CalendarRepository[],
notificationRepositories: Record<string, NotificationRepository>,
reminderSettingsRepository: ReminderSettingsRepository,
): Promise<void> {
type ProcessReminderParams = {
baseTime: Date
calendarRepositories: CalendarRepository[]
groupRepository?: GroupRepository
notificationRepositories: Record<string, NotificationRepository>
reminderSettingsRepository: ReminderSettingsRepository
}

export async function processReminders({
baseTime,
calendarRepositories,
groupRepository,
notificationRepositories,
reminderSettingsRepository,
}: ProcessReminderParams): Promise<void> {
const eta = new Eta()

// Get all events first
const events = (
await Promise.all(
calendarRepositories.map((calendarRepository) => calendarRepository.getEvents(baseTime, addDays(baseTime, 1))),
calendarRepositories.map((calendarRepository) =>

Check failure on line 55 in packages/usecase/src/process_reminders.ts

View workflow job for this annotation

GitHub Actions / test (20)

src/process_reminders.test.ts > processReminders > should send notifications for events with matching reminder times

TypeError: Cannot read properties of undefined (reading 'map') ❯ Module.processReminders src/process_reminders.ts:55:28 ❯ src/process_reminders.test.ts:87:11

Check failure on line 55 in packages/usecase/src/process_reminders.ts

View workflow job for this annotation

GitHub Actions / test (20)

src/process_reminders.test.ts > processReminders > should send notifications for day before at specific hour

TypeError: Cannot read properties of undefined (reading 'map') ❯ Module.processReminders src/process_reminders.ts:55:28 ❯ src/process_reminders.test.ts:141:11

Check failure on line 55 in packages/usecase/src/process_reminders.ts

View workflow job for this annotation

GitHub Actions / test (20)

src/process_reminders.test.ts > processReminders > should handle multiple attendees for the same event

TypeError: Cannot read properties of undefined (reading 'map') ❯ Module.processReminders src/process_reminders.ts:55:28 ❯ src/process_reminders.test.ts:182:11

Check failure on line 55 in packages/usecase/src/process_reminders.ts

View workflow job for this annotation

GitHub Actions / test (20)

src/process_reminders.test.ts > processReminders > should handle multiple reminder settings for the same user

TypeError: Cannot read properties of undefined (reading 'map') ❯ Module.processReminders src/process_reminders.ts:55:28 ❯ src/process_reminders.test.ts:236:11

Check failure on line 55 in packages/usecase/src/process_reminders.ts

View workflow job for this annotation

GitHub Actions / test (20)

src/process_reminders.test.ts > processReminders > should not send notifications for users without reminder settings

TypeError: Cannot read properties of undefined (reading 'map') ❯ Module.processReminders src/process_reminders.ts:55:28 ❯ src/process_reminders.test.ts:273:11

Check failure on line 55 in packages/usecase/src/process_reminders.ts

View workflow job for this annotation

GitHub Actions / test (20)

src/process_reminders.test.ts > processReminders > should not fetch events if no calendars are available

TypeError: Cannot read properties of undefined (reading 'map') ❯ Module.processReminders src/process_reminders.ts:55:28 ❯ src/process_reminders.test.ts:288:11
// FIXME: consider reminder settings
getEvents({ calendarRepository, groupRepository, minDate: baseTime, maxDate: addDays(baseTime, 1) }),
),
)
).flat()
console.debug('Fetched events', events.map((e) => `${e.title} (${e.start})`).join('\n'))
Expand Down

0 comments on commit 9657ff7

Please sign in to comment.