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:
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export const BASE_URL = 'https://api.fireflies.ai/graphql';
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}`
|
||||
@@ -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];
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user