Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,37 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { firefliesAiAuth } from '../../index';
import { getTranscript } from '../common/queries';
import { BASE_URL } from '../common';
export const findMeetingByIdAction = createAction({
auth: firefliesAiAuth,
name: 'find-meeting-by-id',
displayName: 'Find Meeting by ID',
description: 'Finds a specific meeting by ID.',
props: {
meetingId: Property.ShortText({
displayName: 'Meeting ID',
description: 'The ID of the meeting to retrieve.',
required: true,
}),
},
async run(context) {
const response = await httpClient.sendRequest<{ data: { transcript: Record<string, any> } }>({
url: BASE_URL,
method: HttpMethod.POST,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.secret_text,
},
body: {
query: getTranscript,
variables: {
transcriptId: context.propsValue.meetingId,
},
},
});
return response.body.data.transcript;
},
});

View File

@@ -0,0 +1,171 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { firefliesAiAuth } from '../../index';
import { BASE_URL } from '../common';
export const findMeetingByQueryAction = createAction({
auth: firefliesAiAuth,
name: 'find_meeting_by_query',
displayName: 'Find Meeting by Call Deatils',
description: 'Searches meetings based on provided parameters.',
props: {
title: Property.ShortText({
displayName: 'Meeting Title',
required: false,
}),
hostEmail: Property.ShortText({
displayName: 'Host Email',
description: 'Filter meetings by host email.',
required: false,
}),
participantEmail: Property.ShortText({
displayName: 'Participant Email',
description: 'Filter meetings by participant email',
required: false,
}),
date: Property.DateTime({
displayName: 'Date',
description: 'Filter meetings on this date (YYYY-MM-DD).',
required: false,
}),
},
async run({ propsValue, auth }) {
const filterVariables: Record<string, any> = {};
if (propsValue.title) {
filterVariables['title'] = propsValue.title;
}
if (propsValue.hostEmail) {
filterVariables['hostEmail'] = propsValue.hostEmail;
}
if (propsValue.participantEmail) {
filterVariables['participantEmail'] = propsValue.participantEmail;
}
if (propsValue.date) {
// Convert ISO string to milliseconds for the API
const dateMs = new Date(propsValue.date).getTime();
filterVariables['date'] = dateMs;
}
const query = `
query Transcripts(
$title: String
$hostEmail: String
$participantEmail: String
$date: Float
$limit: Int
$skip: Int
) {
transcripts(
title: $title
host_email: $hostEmail
participant_email: $participantEmail
date: $date
limit: $limit
skip: $skip
) {
id
dateString
privacy
speakers
{
id
name
}
title
host_email
organizer_email
calendar_id
user
{
user_id
email
name
num_transcripts
recent_meeting
minutes_consumed
is_admin
integrations
}
fireflies_users
participants
date
transcript_url
audio_url
video_url
duration
meeting_attendees
{
displayName
email
phoneNumber
name
location
}
summary
{
keywords
action_items
outline
shorthand_bullet
overview
bullet_gist
gist
short_summary
short_overview
meeting_type
topics_discussed
transcript_chapters
}
cal_id
calendar_type
meeting_link
}
}
`;
const limit = 50;
let skip = 0;
let hasMore = true;
const meetings = [];
while (hasMore) {
const variables = {
...filterVariables,
limit,
skip,
};
const response = await httpClient.sendRequest<{
data: { transcripts: Record<string, any>[] };
}>({
url: BASE_URL,
method: HttpMethod.POST,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.secret_text,
},
body: {
query: query,
variables,
},
});
const transcripts = response?.body?.data?.transcripts || [];
if (transcripts.length === 0) {
hasMore = false;
} else {
meetings.push(...transcripts);
skip += transcripts.length;
}
}
return {
found: meetings.length !== 0,
meetings,
};
},
});

View File

@@ -0,0 +1,76 @@
import { createAction } from '@activepieces/pieces-framework';
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { firefliesAiAuth } from '../../index';
import { getTranscript } from '../common/queries';
import { isNil } from '@activepieces/shared';
import { BASE_URL } from '../common';
export const findRecentMeetingAction = createAction({
auth: firefliesAiAuth,
name: 'find_recent_meeting',
displayName: 'Find Recent Meeting',
description: 'Retrieves the latest meeting for a user.',
props: {},
async run(context) {
const userResponse = await httpClient.sendRequest<{
data: { user: { recent_meeting?: string } };
}>({
url: BASE_URL,
method: HttpMethod.POST,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.secret_text,
},
body: {
query: `
query User
{
user
{
user_id
recent_transcript
recent_meeting
num_transcripts
name
minutes_consumed
is_admin
integrations
email
}
}`,
variables: {},
},
});
console.log(JSON.stringify(userResponse, null, 2));
if (isNil(userResponse.body.data.user.recent_meeting)) {
return {
found: false,
meeting: {},
};
}
const meetingResponse = await httpClient.sendRequest<{
data: { transcript: Record<string, any> };
}>({
url: BASE_URL,
method: HttpMethod.POST,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.secret_text,
},
body: {
query: getTranscript,
variables: {
transcriptId: userResponse.body.data.user.recent_meeting,
},
},
});
return {
found: true,
meeting: meetingResponse.body.data.transcript,
};
},
});

View File

@@ -0,0 +1,36 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { firefliesAiAuth } from '../../index';
import { getUser } from '../common/queries';
import { BASE_URL } from '../common';
export const getUserDetailsAction = createAction({
auth: firefliesAiAuth,
name: 'get-user-details',
displayName: 'Get User Details',
description: 'Retrieves profile information by ID.',
props: {
userId: Property.ShortText({
displayName: 'User ID',
required: true,
}),
},
async run(context) {
const response = await httpClient.sendRequest<{ data: { user: Record<string, any> } }>({
url: BASE_URL,
method: HttpMethod.POST,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.secret_text,
},
body: {
query: getUser,
variables: {
userId: context.propsValue.userId,
},
},
});
return response.body.data.user;
},
});

View File

@@ -0,0 +1,65 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { firefliesAiAuth } from '../../index';
import { BASE_URL } from '../common';
export const uploadAudioAction = createAction({
auth: firefliesAiAuth,
name: 'upload_audio',
displayName: 'Upload Audio',
description:
'Creates a new meeeting in Fireflies for transcription (requires a publicly accessible URL).',
props: {
audioUrl: Property.ShortText({
displayName: 'Audio URL',
description:
'The publicly accessible URL to your audio file (mp3, mp4, wav, m4a, ogg). Fireflies API only accepts URLs, not direct file uploads. For private files, consider using signed URLs with short expiry times.',
required: true,
}),
title: Property.ShortText({
displayName: 'Title',
required: true,
}),
},
async run({ propsValue, auth }) {
// GraphQL mutation for uploading audio
const query = `
mutation uploadAudio($input: AudioUploadInput) {
uploadAudio(input: $input) {
success
title
message
}
}
`;
// Define interface for the input
interface AudioUploadInput {
url: string;
title: string;
}
// Create input for the mutation with proper typing
const input: AudioUploadInput = {
url: propsValue.audioUrl,
title: propsValue.title,
};
const response = await httpClient.sendRequest({
url: BASE_URL,
method: HttpMethod.POST,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.secret_text,
},
body: {
query,
variables: {
input,
},
},
});
return response.body;
},
});

View File

@@ -0,0 +1 @@
export const BASE_URL = 'https://api.fireflies.ai/graphql';

View File

@@ -0,0 +1,94 @@
export const getTranscript =
`
query Transcript($transcriptId: String!)
{
transcript(id: $transcriptId)
{
id
dateString
privacy
speakers
{
id
name
}
title
host_email
organizer_email
calendar_id
user
{
user_id
email
name
num_transcripts
recent_meeting
minutes_consumed
is_admin
integrations
}
fireflies_users
participants
date
transcript_url
audio_url
video_url
duration
meeting_attendees
{
displayName
email
phoneNumber
name
location
}
summary
{
keywords
action_items
outline
shorthand_bullet
overview
bullet_gist
gist
short_summary
short_overview
meeting_type
topics_discussed
transcript_chapters
}
cal_id
calendar_type
meeting_info
{
fred_joined
silent_meeting
summary_status
}
meeting_link
}
}
`
export const getUser =
`
query User($userId: String!)
{
user(id: $userId)
{
user_id
recent_transcript
recent_meeting
num_transcripts
name
minutes_consumed
is_admin
integrations
email
user_groups
{
name
handle
}
}
}`

View File

@@ -0,0 +1,155 @@
import { createTrigger, Property, TriggerStrategy } from '@activepieces/pieces-framework';
import { firefliesAiAuth } from '../../index';
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { getTranscript } from '../common/queries';
import { BASE_URL } from '../common';
export const newTranscriptionCompletedTrigger = createTrigger({
auth: firefliesAiAuth,
name: 'new_transcription_completed',
displayName: 'New Transcription Completed',
description: 'Triggered when a new meeting is transcribed.',
props: {
webhookInstructions: Property.MarkDown({
value: `
## Fireflies.ai Webhook Setup
To use this trigger, you need to manually set up a webhook in your Fireflies.ai account:
1. Login to your Fireflies.ai account.
2. Navigate to **Settings** > **Developer Settings** in the left sidebar.
3. Enter the following URL in the webhooks field:
\`\`\`text
{{webhookUrl}}
\`\`\`
4. Click Save to register the webhook.
This webhook will be triggered when a meeting transcription is completed.
`,
}),
},
type: TriggerStrategy.WEBHOOK,
sampleData: undefined,
async onEnable(context) {
// No need to register webhooks programmatically as user will do it manually
},
async onDisable(context) {
// No need to unregister webhooks as user will do it manually
},
async test(context) {
const query = `
query Transcripts(
$limit: Int
$skip: Int
){
transcripts(
limit: $limit
skip: $skip
){
id
dateString
privacy
speakers
{
id
name
}
title
host_email
organizer_email
calendar_id
user
{
user_id
email
name
num_transcripts
recent_meeting
minutes_consumed
is_admin
integrations
}
fireflies_users
participants
date
transcript_url
audio_url
video_url
duration
meeting_attendees
{
displayName
email
phoneNumber
name
location
}
summary
{
keywords
action_items
outline
shorthand_bullet
overview
bullet_gist
gist
short_summary
short_overview
meeting_type
topics_discussed
transcript_chapters
}
cal_id
calendar_type
meeting_info
{
fred_joined
silent_meeting
summary_status
}
meeting_link
}
}`;
const response = await httpClient.sendRequest<{ data: { transcripts: Record<string, any>[] } }>(
{
url: BASE_URL,
method: HttpMethod.POST,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.secret_text,
},
body: {
query,
variables: {
limit: 5,
skip: 0,
},
},
},
);
return response.body.data.transcripts;
},
async run(context) {
const payload = context.payload.body as { meetingId: string; eventType: string };
if (payload.eventType !== 'Transcription completed') {
return [];
}
const response = await httpClient.sendRequest<{ data: { transcript: Record<string, any> } }>({
url:BASE_URL,
method: HttpMethod.POST,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.secret_text,
},
body: {
query: getTranscript,
variables: {
transcriptId: payload.meetingId,
},
},
});
return [response.body.data.transcript];
},
});