From 72be1284370c39e47a877ee1c743b3cb089a5198 Mon Sep 17 00:00:00 2001 From: Janhvi Patil Date: Sun, 7 Apr 2024 22:18:07 +0530 Subject: [PATCH 1/4] fix: message preview for poll on mobile --- .../features/chat-space/MessageActions/MessageActionModal.tsx | 4 ++-- .../features/chat-space/MessageActions/MessagePreview.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx b/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx index f47353678..0ac75841f 100644 --- a/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx +++ b/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx @@ -77,8 +77,8 @@ export const MessageActionModal = ({ selectedMessage, onDismiss }: MessageAction Reply */} - - + {selectedMessage.message_type !== 'Poll' && } + {selectedMessage.message_type !== 'Poll' && } {isOwnMessage && diff --git a/mobile/src/components/features/chat-space/MessageActions/MessagePreview.tsx b/mobile/src/components/features/chat-space/MessageActions/MessagePreview.tsx index 3060a8bec..383888bb4 100644 --- a/mobile/src/components/features/chat-space/MessageActions/MessagePreview.tsx +++ b/mobile/src/components/features/chat-space/MessageActions/MessagePreview.tsx @@ -1,4 +1,4 @@ -import { Message } from '../../../../../../types/Messaging/Message' +import { Message, PollMessage } from '../../../../../../types/Messaging/Message' import { UserFields } from '@/utils/users/UserListProvider' import { MessageSenderAvatar } from '../chat-view/MessageBlock' import { IonText } from '@ionic/react' @@ -25,6 +25,8 @@ const MessagePreview = ({ message, user }: MessagePreview) => { } {message.message_type === 'File' &&

📎  {message.file?.split('/')[3]}

} + {message.message_type === 'Poll' &&

+ 📊  Poll: {(message as PollMessage).content?.split("\n")?.[0]}

} ) From 511979255073f7788c4ace8825450fad4f4f1600 Mon Sep 17 00:00:00 2001 From: Janhvi Patil Date: Mon, 8 Apr 2024 13:58:21 +0530 Subject: [PATCH 2/4] feat: retract vote on web --- .../MessageActions/MessageActions.tsx | 4 ++ .../MessageActions/RetractVote.tsx | 56 +++++++++++++++++++ .../ChatMessage/Renderers/PollMessage.tsx | 14 ++--- raven/api/raven_poll.py | 15 +++++ raven/permissions.py | 2 +- .../raven_poll_vote/raven_poll_vote.json | 3 +- 6 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 raven-app/src/components/feature/chat/ChatMessage/MessageActions/RetractVote.tsx diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx index ede5eea68..4646495f9 100644 --- a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx @@ -7,6 +7,7 @@ import { HiReply } from 'react-icons/hi' import { FrappeConfig, FrappeContext } from 'frappe-react-sdk' import { useMessageCopy } from './useMessageCopy' import { useToast } from '@/hooks/useToast' +import { RetractVote } from './RetractVote' export interface MessageContextMenuProps { message?: Message | null, @@ -25,6 +26,9 @@ export const MessageContextMenu = ({ message, onDelete, onEdit, onReply }: Messa return ( {message ? <> + + {message && message.message_type === 'Poll' && } + diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/RetractVote.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/RetractVote.tsx new file mode 100644 index 000000000..b6f09629c --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/RetractVote.tsx @@ -0,0 +1,56 @@ +import { ContextMenu, Flex } from '@radix-ui/themes' +import { useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk' +import { TiArrowBackOutline } from 'react-icons/ti' +import { Poll } from '../Renderers/PollMessage' +import { Message } from '../../../../../../../types/Messaging/Message' +import { toast } from '@/hooks/useToast' + +interface RetractVoteProps { + message: Message +} + +export const RetractVote = ({ message }: RetractVoteProps) => { + + // fetch poll data using message_id + const { data } = useFrappeGetCall<{ message: Poll }>('raven.api.raven_poll.get_poll', { + 'message_id': message?.name, + }, `poll_data_${message?.poll_id}`, { + revalidateOnFocus: false, + revalidateIfStale: false, + revalidateOnReconnect: false + }) + + const { call } = useFrappePostCall('raven.api.raven_poll.retract_vote') + const onRetractVote = () => { + return call({ + poll_id: message?.poll_id, + }).then(() => { + toast({ + title: 'Vote retracted', + variant: 'accent', + duration: 800, + }) + }).catch(() => { + toast({ + title: 'Could not retract vote', + variant: 'destructive', + duration: 800, + }) + }) + } + + if (data && data.message?.current_user_votes.length > 0) + return ( + <> + + + + Retract vote + + + + + ) + + return null +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx index 638b83b6b..1da40427a 100644 --- a/raven-app/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx @@ -1,9 +1,9 @@ import { Box, Checkbox, Flex, Text, RadioGroup, Button, Badge } from "@radix-ui/themes" import { BoxProps } from "@radix-ui/themes/dist/cjs/components/box" -import { memo, useEffect, useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { UserFields } from "../../../../../utils/users/UserListProvider" import { PollMessage } from "../../../../../../../types/Messaging/Message" -import { useFrappeDocumentEventListener, useFrappeGetCall, useFrappePostCall, useSWRConfig } from "frappe-react-sdk" +import { useFrappeDocumentEventListener, useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk" import { RavenPoll } from "@/types/RavenMessaging/RavenPoll" import { ErrorBanner } from "@/components/layout/AlertBanner" import { RavenPollOption } from "@/types/RavenMessaging/RavenPollOption" @@ -14,12 +14,12 @@ interface PollMessageBlockProps extends BoxProps { user?: UserFields, } -interface Poll { +export interface Poll { 'poll': RavenPoll, 'current_user_votes': { 'option': string }[] } -export const PollMessageBlock = memo(({ message, user, ...props }: PollMessageBlockProps) => { +export const PollMessageBlock = ({ message, user, ...props }: PollMessageBlockProps) => { // fetch poll data using message_id const { data, error, mutate } = useFrappeGetCall<{ message: Poll }>('raven.api.raven_poll.get_poll', { @@ -40,7 +40,7 @@ export const PollMessageBlock = memo(({ message, user, ...props }: PollMessageBl {data && } ) -}) +} const PollMessageBox = ({ data, messageID }: { data: Poll, messageID: string }) => { return ( @@ -132,7 +132,6 @@ const PollOption = ({ data, option }: { data: Poll, option: RavenPollOption }) = const SingleChoicePoll = ({ data, messageID }: { data: Poll, messageID: string }) => { - const { mutate } = useSWRConfig() const { call } = useFrappePostCall('raven.api.raven_poll.add_vote') const { toast } = useToast() const onVoteSubmit = async (option: RavenPollOption) => { @@ -140,7 +139,6 @@ const SingleChoicePoll = ({ data, messageID }: { data: Poll, messageID: string } 'message_id': messageID, 'option_id': option.name }).then(() => { - mutate(`poll_data_${data.poll.name}`) toast({ title: "Your vote has been submitted!", variant: 'success', @@ -168,7 +166,6 @@ const SingleChoicePoll = ({ data, messageID }: { data: Poll, messageID: string } const MultiChoicePoll = ({ data, messageID }: { data: Poll, messageID: string }) => { const [selectedOptions, setSelectedOptions] = useState([]) - const { mutate } = useSWRConfig() const { toast } = useToast() const handleCheckboxChange = (name: string, value: boolean | string) => { @@ -185,7 +182,6 @@ const MultiChoicePoll = ({ data, messageID }: { data: Poll, messageID: string }) 'message_id': messageID, 'option_id': selectedOptions }).then(() => { - mutate(`poll_data_${data.poll.name}`) toast({ title: "Your vote has been submitted!", variant: 'success', diff --git a/raven/api/raven_poll.py b/raven/api/raven_poll.py index d59fb6871..bb82dda4d 100644 --- a/raven/api/raven_poll.py +++ b/raven/api/raven_poll.py @@ -113,3 +113,18 @@ def add_vote(message_id, option_id): ).insert() return "Vote added successfully." + + +@frappe.whitelist(methods=["POST"]) +def retract_vote(poll_id): + # delete all votes by the user for the poll (this takes care of the case where the user has voted for multiple options in the same poll) + user = frappe.session.user + votes = frappe.get_all( + "Raven Poll Vote", + filters={"poll_id": poll_id, "user_id": user}, + fields=["name"]) + if not votes: + frappe.throw(_("You have not voted for any option in this poll.")) + else: + for vote in votes: + frappe.delete_doc("Raven Poll Vote", vote.name) \ No newline at end of file diff --git a/raven/permissions.py b/raven/permissions.py index 04227d5ea..36af8aec8 100644 --- a/raven/permissions.py +++ b/raven/permissions.py @@ -76,7 +76,7 @@ def raven_poll_vote_has_permission(doc, user=None, ptype=None): Allowed users can add a vote to a poll and read votes (if the poll is not anonymous) """ - if ptype in ["read", "create"]: + if ptype in ["read", "create", "delete"]: if doc.owner == user: return True elif user == "Administrator": diff --git a/raven/raven_messaging/doctype/raven_poll_vote/raven_poll_vote.json b/raven/raven_messaging/doctype/raven_poll_vote/raven_poll_vote.json index dea96348a..751c9f845 100644 --- a/raven/raven_messaging/doctype/raven_poll_vote/raven_poll_vote.json +++ b/raven/raven_messaging/doctype/raven_poll_vote/raven_poll_vote.json @@ -53,7 +53,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-03-22 20:08:19.982449", + "modified": "2024-04-08 13:32:30.594728", "modified_by": "Administrator", "module": "Raven Messaging", "name": "Raven Poll Vote", @@ -73,6 +73,7 @@ }, { "create": 1, + "delete": 1, "email": 1, "export": 1, "if_owner": 1, From 6132d1d3887f469d13b5aae64fb899e9708d9ca9 Mon Sep 17 00:00:00 2001 From: Janhvi Patil Date: Mon, 8 Apr 2024 20:15:24 +0530 Subject: [PATCH 3/4] feat: added retract vote in mobile --- .../MessageActions/MessageActionModal.tsx | 15 ++++-- .../MessageActions/RetractVoteAction.tsx | 50 +++++++++++++++++++ .../chat-space/chat-view/MessageBlock.tsx | 2 +- 3 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 mobile/src/components/features/chat-space/MessageActions/RetractVoteAction.tsx diff --git a/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx b/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx index 0ac75841f..ca9c1dd9b 100644 --- a/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx +++ b/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx @@ -13,6 +13,7 @@ import { SaveMessageAction } from './SaveMessageAction'; import { useGetUser } from '@/hooks/useGetUser'; import { ShareAction } from './ShareAction'; import { EmojiAction } from './EmojiAction'; +import { RetractVoteAction } from './RetractVoteAction'; import MessagePreview from './MessagePreview'; interface MessageActionModalProps { @@ -77,13 +78,19 @@ export const MessageActionModal = ({ selectedMessage, onDismiss }: MessageAction Reply */} - {selectedMessage.message_type !== 'Poll' && } - {selectedMessage.message_type !== 'Poll' && } + + {selectedMessage.message_type === 'Poll' && + } + {selectedMessage.message_type !== 'Poll' && + } + + {selectedMessage.message_type !== 'Poll' && + } + {isOwnMessage && - - } + } {/* Link to document diff --git a/mobile/src/components/features/chat-space/MessageActions/RetractVoteAction.tsx b/mobile/src/components/features/chat-space/MessageActions/RetractVoteAction.tsx new file mode 100644 index 000000000..bede8f480 --- /dev/null +++ b/mobile/src/components/features/chat-space/MessageActions/RetractVoteAction.tsx @@ -0,0 +1,50 @@ +import { useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk' +import { ActionIcon, ActionItem, ActionLabel, ActionProps } from './common' +import { arrowUndoOutline } from 'ionicons/icons' +import { useIonToast } from '@ionic/react' +import { Poll } from '../chat-view/MessageBlock' + +export const RetractVoteAction = ({ message, onSuccess }: ActionProps) => { + + const [present] = useIonToast() + + // fetch poll data using message_id + const { data } = useFrappeGetCall<{ message: Poll }>('raven.api.raven_poll.get_poll', { + 'message_id': message?.name, + }, `poll_data_${message?.poll_id}`, { + revalidateOnFocus: false, + revalidateIfStale: false, + revalidateOnReconnect: false + }) + + const { call } = useFrappePostCall('raven.api.raven_poll.retract_vote') + const onRetractVote = () => { + return call({ + poll_id: message?.poll_id, + }).then(() => { + present({ + position: 'bottom', + color: 'success', + duration: 600, + message: 'Vote retracted', + }) + onSuccess() + }).catch(() => { + present({ + color: 'danger', + duration: 600, + message: "Error: Could not retract vote", + }) + }) + } + + if (data && data.message?.current_user_votes.length > 0) + return ( + + + + + ) + + return null +} \ No newline at end of file diff --git a/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx b/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx index ff32927c4..a93fd44d0 100644 --- a/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx +++ b/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx @@ -301,7 +301,7 @@ const ReplyBlock = ({ message }: { message: Message }) => { } -interface Poll { +export interface Poll { 'poll': RavenPoll, 'current_user_votes': { 'option': string }[] } From 97acc52dfd24ac27bde4edccf43dae8242beaf41 Mon Sep 17 00:00:00 2001 From: Janhvi Patil Date: Mon, 8 Apr 2024 20:22:10 +0530 Subject: [PATCH 4/4] chore: run pre-commit --- raven/api/chat.py | 24 +-- raven/api/raven_poll.py | 9 +- .../raven_scheduler_event.py | 139 +++++++------ .../doctype/raven_webhook/raven_webhook.py | 186 ++++++++---------- 4 files changed, 167 insertions(+), 191 deletions(-) diff --git a/raven/api/chat.py b/raven/api/chat.py index 519dc0343..68ea867fc 100644 --- a/raven/api/chat.py +++ b/raven/api/chat.py @@ -19,18 +19,18 @@ def get_channel_members(channel_id): if frappe.get_cached_value("Raven Channel", channel_id, "type") == "Open": # select all users, if channel member exists, get is_admin member_query = ( - frappe.qb.from_(user) - .join(channel_member, JoinType.left) - .on((user.name == channel_member.user_id) & (channel_member.channel_id == channel_id)) - .select( - user.name, - user.full_name, - user.user_image, - user.first_name, - user.type, - channel_member.is_admin, - ) - .orderby(channel_member.creation, order=Order.desc) + frappe.qb.from_(user) + .join(channel_member, JoinType.left) + .on((user.name == channel_member.user_id) & (channel_member.channel_id == channel_id)) + .select( + user.name, + user.full_name, + user.user_image, + user.first_name, + user.type, + channel_member.is_admin, + ) + .orderby(channel_member.creation, order=Order.desc) ) else: member_query = ( diff --git a/raven/api/raven_poll.py b/raven/api/raven_poll.py index bb82dda4d..dc395f41c 100644 --- a/raven/api/raven_poll.py +++ b/raven/api/raven_poll.py @@ -120,11 +120,10 @@ def retract_vote(poll_id): # delete all votes by the user for the poll (this takes care of the case where the user has voted for multiple options in the same poll) user = frappe.session.user votes = frappe.get_all( - "Raven Poll Vote", - filters={"poll_id": poll_id, "user_id": user}, - fields=["name"]) + "Raven Poll Vote", filters={"poll_id": poll_id, "user_id": user}, fields=["name"] + ) if not votes: frappe.throw(_("You have not voted for any option in this poll.")) - else: + else: for vote in votes: - frappe.delete_doc("Raven Poll Vote", vote.name) \ No newline at end of file + frappe.delete_doc("Raven Poll Vote", vote.name) diff --git a/raven/raven_integrations/doctype/raven_scheduler_event/raven_scheduler_event.py b/raven/raven_integrations/doctype/raven_scheduler_event/raven_scheduler_event.py index 127fc204f..a204e9ae7 100644 --- a/raven/raven_integrations/doctype/raven_scheduler_event/raven_scheduler_event.py +++ b/raven/raven_integrations/doctype/raven_scheduler_event/raven_scheduler_event.py @@ -6,83 +6,82 @@ class RavenSchedulerEvent(Document): - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. - from typing import TYPE_CHECKING + from typing import TYPE_CHECKING - if TYPE_CHECKING: - from frappe.types import DF + if TYPE_CHECKING: + from frappe.types import DF - bot: DF.Link - channel: DF.Link - content: DF.SmallText - cron_expression: DF.Data | None - disabled: DF.Check - dm: DF.Link | None - event_frequency: DF.Literal["Every Day", - "Every Day of the week", "Date of the month", "Cron"] - event_name: DF.Data - scheduler_event_id: DF.Link | None - send_to: DF.Literal["Channel", "DM"] - # end: auto-generated types + bot: DF.Link + channel: DF.Link + content: DF.SmallText + cron_expression: DF.Data | None + disabled: DF.Check + dm: DF.Link | None + event_frequency: DF.Literal["Every Day", "Every Day of the week", "Date of the month", "Cron"] + event_name: DF.Data + scheduler_event_id: DF.Link | None + send_to: DF.Literal["Channel", "DM"] + # end: auto-generated types - def before_save(self): - ''' - 1. If the 'scheduler_event_id' is not set, create a Server Script of type 'Scheduler Event' and set the 'scheduler_event_id' to the name of the Server Script. - ''' - if not self.scheduler_event_id: - self.scheduler_event_id = self.create_scheduler_event() - else: - server_script = frappe.get_doc( - 'Server Script', self.scheduler_event_id) - server_script.cron_format = self.cron_expression - server_script.script = self.get_scheduler_event_script() - server_script.save() + def before_save(self): + """ + 1. If the 'scheduler_event_id' is not set, create a Server Script of type 'Scheduler Event' and set the 'scheduler_event_id' to the name of the Server Script. + """ + if not self.scheduler_event_id: + self.scheduler_event_id = self.create_scheduler_event() + else: + server_script = frappe.get_doc("Server Script", self.scheduler_event_id) + server_script.cron_format = self.cron_expression + server_script.script = self.get_scheduler_event_script() + server_script.save() - def on_update(self): - ''' - 1. If the 'scheduler_event_id' is set, and the 'disabled' field is updated, update the 'disabled' field of the Server Script of type 'Scheduler Event' with the name 'scheduler_event_id'. - ''' - if self.scheduler_event_id: - server_script = frappe.get_doc( - 'Server Script', self.scheduler_event_id) - server_script.disabled = self.disabled - server_script.save() + def on_update(self): + """ + 1. If the 'scheduler_event_id' is set, and the 'disabled' field is updated, update the 'disabled' field of the Server Script of type 'Scheduler Event' with the name 'scheduler_event_id'. + """ + if self.scheduler_event_id: + server_script = frappe.get_doc("Server Script", self.scheduler_event_id) + server_script.disabled = self.disabled + server_script.save() - def on_trash(self): - ''' - 1. If the 'scheduler_event_id' is set, delete the Server Script of type 'Scheduler Event' with the name 'scheduler_event_id'. - ''' - if self.scheduler_event_id: - frappe.delete_doc('Server Script', self.scheduler_event_id) + def on_trash(self): + """ + 1. If the 'scheduler_event_id' is set, delete the Server Script of type 'Scheduler Event' with the name 'scheduler_event_id'. + """ + if self.scheduler_event_id: + frappe.delete_doc("Server Script", self.scheduler_event_id) - def create_scheduler_event(self): - ''' - Create a Server Script of type 'Scheduler Event' and set the 'scheduler_event_id' to the name of the Server Script. - ''' - server_script = frappe.get_doc({ - 'doctype': 'Server Script', - 'script_type': 'Scheduler Event', - 'name': self.event_name, - 'disabled': 0, - 'event_frequency': 'Cron', - 'cron_format': self.cron_expression, - 'script': self.get_scheduler_event_script() - }) - server_script.insert() - return server_script.name + def create_scheduler_event(self): + """ + Create a Server Script of type 'Scheduler Event' and set the 'scheduler_event_id' to the name of the Server Script. + """ + server_script = frappe.get_doc( + { + "doctype": "Server Script", + "script_type": "Scheduler Event", + "name": self.event_name, + "disabled": 0, + "event_frequency": "Cron", + "cron_format": self.cron_expression, + "script": self.get_scheduler_event_script(), + } + ) + server_script.insert() + return server_script.name - def get_scheduler_event_script(self): - ''' - Get the script for the Scheduler Event - ''' - # bot = frappe.get_doc('Raven Bot', self.bot) - # bot.send_message(self.channel, {'text': self.content}) - # return code snippet with bot & content as values - message = {'text': self.content} - script = f''' + def get_scheduler_event_script(self): + """ + Get the script for the Scheduler Event + """ + # bot = frappe.get_doc('Raven Bot', self.bot) + # bot.send_message(self.channel, {'text': self.content}) + # return code snippet with bot & content as values + message = {"text": self.content} + script = f""" bot = frappe.get_doc('Raven Bot', '{self.bot}')\n bot.send_message('{self.channel}', {message}) -''' - return script +""" + return script diff --git a/raven/raven_integrations/doctype/raven_webhook/raven_webhook.py b/raven/raven_integrations/doctype/raven_webhook/raven_webhook.py index 862a5c49a..28d453f4e 100644 --- a/raven/raven_integrations/doctype/raven_webhook/raven_webhook.py +++ b/raven/raven_integrations/doctype/raven_webhook/raven_webhook.py @@ -17,8 +17,7 @@ class RavenWebhook(Document): from frappe.types import DF channel_id: DF.Link | None - channel_type: DF.Literal["", "Public", - "Private", "Open", "DM", "Self Message"] + channel_type: DF.Literal["", "Public", "Private", "Open", "DM", "Self Message"] condition: DF.SmallText | None conditions_on: DF.Literal["", "Channel", "User", "Channel Type", "Custom"] enable_security: DF.Check @@ -32,29 +31,36 @@ class RavenWebhook(Document): webhook_data: DF.Table[WebhookData] webhook_headers: DF.Table[WebhookHeader] webhook_secret: DF.Password | None - webhook_trigger: DF.Literal["Message Sent", "Message Edited", "Message Deleted", "Message Reacted On", - "Channel Created", "Channel Deleted", "Channel Member Added", "Channel Member Deleted", "User Added", "User Deleted"] + webhook_trigger: DF.Literal[ + "Message Sent", + "Message Edited", + "Message Deleted", + "Message Reacted On", + "Channel Created", + "Channel Deleted", + "Channel Member Added", + "Channel Member Deleted", + "User Added", + "User Deleted", + ] # end: auto-generated types def validate(self): # 1. Check if webhook name is unique # 2. Check if webhook_data and webhook_headers are unique - - # 1. Check if webhook name is unique - webhook = frappe.get_all('Raven Webhook', filters={ - 'webhook_name': self.name}) + webhook = frappe.get_all("Raven Webhook", filters={"webhook_name": self.name}) if webhook: - frappe.throw('Webhook name already exists') + frappe.throw("Webhook name already exists") # 2. Check if webhook_data and webhook_headers are unique webhook_data_keys = [data.key for data in self.webhook_data] webhook_header_keys = [data.key for data in self.webhook_headers] if len(webhook_data_keys) != len(set(webhook_data_keys)): - frappe.throw('Webhook Data keys should be unique') + frappe.throw("Webhook Data keys should be unique") if len(webhook_header_keys) != len(set(webhook_header_keys)): - frappe.throw('Webhook Headers keys should be unique') + frappe.throw("Webhook Headers keys should be unique") def before_save(self): # 1. Check if webhook ID is exists @@ -70,26 +76,25 @@ def before_save(self): # 3. Create the webhook self.create_webhook() - def on_trash(self): # Delete the webhook if self.webhook: - frappe.db.delete('Webhook', self.webhook) + frappe.db.delete("Webhook", self.webhook) def create_webhook(self): # Create a new webhook doctype, event = self.get_doctype_and_event() conditions = self.get_conditions() - webhook_doc = frappe.new_doc('Webhook') + webhook_doc = frappe.new_doc("Webhook") webhook_doc.name = self.name webhook_doc.request_url = self.request_url webhook_doc.is_dynamic_url = self.is_dynamic_url webhook_doc.timeout = self.timeout webhook_doc.enable_security = self.enable_security webhook_doc.webhook_secret = self.webhook_secret - webhook_doc.request_method = 'POST' - webhook_doc.request_structure = 'Form URL-Encoded' + webhook_doc.request_method = "POST" + webhook_doc.request_structure = "Form URL-Encoded" self.set_webhook_data_and_headers(webhook_doc) webhook_doc.webhook_doctype = doctype webhook_doc.webhook_docevent = event @@ -101,7 +106,7 @@ def update_webhook(self): # Update the webhook conditions = self.get_conditions() - webhook_doc = frappe.get_doc('Webhook', self.webhook) + webhook_doc = frappe.get_doc("Webhook", self.webhook) webhook_doc.request_url = self.request_url webhook_doc.is_dynamic_url = self.is_dynamic_url webhook_doc.timeout = self.timeout @@ -115,8 +120,8 @@ def set_webhook_data_and_headers(self, webhook_doc): # Set the webhook data and headers # get the existing webhook data and headers - webhook_header = webhook_doc.get('webhook_headers', []) - webhook_data = webhook_doc.get('webhook_data', []) + webhook_header = webhook_doc.get("webhook_headers", []) + webhook_data = webhook_doc.get("webhook_data", []) # get the existing webhook data and headers keys webhook_data_keys = [data.key for data in self.webhook_data] @@ -127,81 +132,54 @@ def set_webhook_data_and_headers(self, webhook_doc): # and append the new webhook data and headers for data in self.webhook_data: if data.key not in webhook_data_keys: - webhook_doc.append('webhook_data', { - 'key': data.key, - 'fieldname': data.fieldname, - }) + webhook_doc.append( + "webhook_data", + { + "key": data.key, + "fieldname": data.fieldname, + }, + ) for data in self.webhook_headers: if data.key not in webhook_header_keys: - webhook_doc.append('webhook_headers', { - 'key': data.key, - 'value': data.value, - }) + webhook_doc.append( + "webhook_headers", + { + "key": data.key, + "value": data.value, + }, + ) def get_doctype_and_event(self): doctypes_and_events = [ { - 'label': 'Message Sent', - 'doctype': 'Raven Message', - 'event': 'after_insert', - }, - { - - 'label': 'Message Edited', - 'doctype': 'Raven Message', - 'event': 'on_update' - }, - { - - 'label': 'Message Deleted', - 'doctype': 'Raven Message', - 'event': 'on_trash' - }, - { - - 'label': 'Message Reacted On', - 'doctype': 'Raven Message Reaction', - 'event': 'after_insert' - }, - { - - 'label': 'Channel Created', - 'doctype': 'Raven Channel', - 'event': 'after_insert' + "label": "Message Sent", + "doctype": "Raven Message", + "event": "after_insert", }, + {"label": "Message Edited", "doctype": "Raven Message", "event": "on_update"}, + {"label": "Message Deleted", "doctype": "Raven Message", "event": "on_trash"}, + {"label": "Message Reacted On", "doctype": "Raven Message Reaction", "event": "after_insert"}, + {"label": "Channel Created", "doctype": "Raven Channel", "event": "after_insert"}, + {"label": "Channel Deleted", "doctype": "Raven Channel", "event": "on_trash"}, { - - 'label': 'Channel Deleted', - 'doctype': 'Raven Channel', - 'event': 'on_trash' + "label": "Member Added to the Channel", + "doctype": "Raven Channel Member", + "event": "after_insert", }, { - 'label': 'Member Added to the Channel', - 'doctype': 'Raven Channel Member', - 'event': 'after_insert' + "label": "Member Deleted from the Channel", + "doctype": "Raven Channel Member", + "event": "on_trash", }, - { - 'label': 'Member Deleted from the Channel', - 'doctype': 'Raven Channel Member', - 'event': 'on_trash' - }, - { - 'label': 'User Added', - 'doctype': 'Raven User', - 'event': 'after_insert' - }, - { - 'label': 'User Deleted', - 'doctype': 'Raven User', - 'event': 'on_trash' - } + {"label": "User Added", "doctype": "Raven User", "event": "after_insert"}, + {"label": "User Deleted", "doctype": "Raven User", "event": "on_trash"}, ] doctype, event = None, None for doctype_and_event in doctypes_and_events: - if self.webhook_trigger == doctype_and_event['label']: - doctype = doctype_and_event['doctype'] - event = doctype_and_event['event'] + if self.webhook_trigger == doctype_and_event["label"]: + doctype = doctype_and_event["doctype"] + event = doctype_and_event["event"] break return doctype, event @@ -209,42 +187,42 @@ def get_conditions(self): # Get the conditions for the webhook doctype, event = self.get_doctype_and_event() if self.trigger_webhook_on_condition: - if self.conditions_on == 'Channel': - if doctype == 'Raven Channel': + if self.conditions_on == "Channel": + if doctype == "Raven Channel": # return 'doc.name == self.channel_id' return f'doc.name == "{self.channel_id}"' - elif doctype == 'Raven Channel Member': + elif doctype == "Raven Channel Member": return f'doc.channel_id == "{self.channel_id}"' - elif doctype == 'Raven Message': + elif doctype == "Raven Message": return f'doc.channel_id == "{self.channel_id}"' - elif doctype == 'Raven Message Reaction': - frappe.throw('Message Reaction cannot be triggered on Channel') - elif doctype == 'Raven User': - frappe.throw('Raven User cannot be triggered on Channel') - - elif self.conditions_on == 'User': - if doctype == 'Raven Channel': - frappe.throw('Channel cannot be triggered on User') - elif doctype == 'Raven Channel Member': + elif doctype == "Raven Message Reaction": + frappe.throw("Message Reaction cannot be triggered on Channel") + elif doctype == "Raven User": + frappe.throw("Raven User cannot be triggered on Channel") + + elif self.conditions_on == "User": + if doctype == "Raven Channel": + frappe.throw("Channel cannot be triggered on User") + elif doctype == "Raven Channel Member": return f'doc.user_id == "{self.user}"' - elif doctype == 'Raven Message': + elif doctype == "Raven Message": return f'doc.owner == "{self.user}"' - elif doctype == 'Raven Message Reaction': + elif doctype == "Raven Message Reaction": return f'doc.owner == "{self.user}"' - elif self.conditions_on == 'Channel Type': - if doctype == 'Raven Channel': - if self.channel_type in ['Public', 'Private', 'Open']: + elif self.conditions_on == "Channel Type": + if doctype == "Raven Channel": + if self.channel_type in ["Public", "Private", "Open"]: return f'doc.type == "{self.channel_type}"' - elif self.channel_type == 'DM': - return f'doc.is_direct_message == 1' - elif self.channel_type == 'Self Message': - return f'doc.is_self_message == 1' + elif self.channel_type == "DM": + return f"doc.is_direct_message == 1" + elif self.channel_type == "Self Message": + return f"doc.is_self_message == 1" else: - frappe.throw('Invalid Channel Type') + frappe.throw("Invalid Channel Type") else: - frappe.throw('Channel Type cannot be triggered on other doctypes') - elif self.conditions_on == 'Custom': + frappe.throw("Channel Type cannot be triggered on other doctypes") + elif self.conditions_on == "Custom": return self.condition return None