Skip to content

Commit 3e4b4e9

Browse files
committed
fix: resolve merge conflict in get_raven_user - complete query execution
2 parents 65ec4dd + 2304d5f commit 3e4b4e9

16 files changed

Lines changed: 348 additions & 27 deletions

File tree

frontend/src/components/feature/chat/ChatMessage/Renderers/FileMessage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const FileMessageBlock = memo(({ message, user, ...props }: FileMessageBl
5151
title="Download"
5252
color='gray'
5353
target='_blank'>{fileName}</Link>
54-
<video src={message.file} controls className="rounded-md shadow-md max-h-96 max-w-[620px]">
54+
<video src={message.file} controls className="rounded-md shadow-md max-h-96 max-w-[620px]" preload="metadata">
5555

5656
</video>
5757
</Flex> :

frontend/src/utils/operations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const getFileExtension = (filename: string) => {
1313
return extension;
1414
}
1515

16-
export const VIDEO_FORMATS = ['mp4', 'webm']
16+
export const VIDEO_FORMATS = ['mp4', 'webm', 'mov']
1717
/**
1818
* Function to check if a file is a video
1919
* @param extension extension of the file

raven/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "2.6.6"
1+
__version__ = "2.7.0"
22

33
from raven.raven_integrations.doctype.raven_incoming_webhook.raven_incoming_webhook import ( # noqa
44
handle_incoming_webhook as webhook,

raven/api/raven_channel.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def create_direct_message_channel(user_id):
160160
Creates a direct message channel between current user and the user with user_id
161161
The user_id can be the peer or the user themself
162162
1. Check if a channel already exists between the two users
163+
- Use dm_user_1 and dm_user_2 to check if a channel exists to prevent race condition duplicates
163164
2. If not, create a new channel
164165
3. Check if the user_id is the current user and set is_self_message accordingly
165166
"""
@@ -177,14 +178,18 @@ def create_direct_message_channel(user_id):
177178
if not get_raven_user(user_id):
178179
frappe.throw(_("The user you are trying to message is not a Raven User."))
179180

181+
# Get the canonical order of the users
182+
if frappe.session.user > user_id:
183+
dm_user_1, dm_user_2 = frappe.session.user, user_id
184+
else:
185+
dm_user_1, dm_user_2 = user_id, frappe.session.user
186+
180187
channel_name = frappe.db.get_value(
181188
"Raven Channel",
182189
filters={
183190
"is_direct_message": 1,
184-
"channel_name": [
185-
"in",
186-
[frappe.session.user + " _ " + user_id, user_id + " _ " + frappe.session.user],
187-
],
191+
"dm_user_1": dm_user_1,
192+
"dm_user_2": dm_user_2,
188193
},
189194
fieldname="name",
190195
)
@@ -195,7 +200,7 @@ def create_direct_message_channel(user_id):
195200
channel = frappe.get_doc(
196201
{
197202
"doctype": "Raven Channel",
198-
"channel_name": frappe.session.user + " _ " + user_id,
203+
"channel_name": dm_user_1 + " _ " + dm_user_2,
199204
"is_direct_message": 1,
200205
"is_self_message": frappe.session.user == user_id,
201206
}

raven/patches.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ raven.patches.v2_0.migrate_existing_dm_threads
1111
raven.patches.v2_0.create_default_workspace
1212
raven.patches.v2_0.create_default_company_workspace_mapping
1313
raven.patches.v2_4.add_unique_constraint_on_reactions #2
14-
raven.patches.v2_5.migrate_ai_bots_to_openai_provider
14+
raven.patches.v2_5.migrate_ai_bots_to_openai_provider
15+
raven.patches.v2_6.clean_duplicate_channel_members
16+
raven.patches.v2_6.add_unique_constraint_in_raven_channel_member
17+
raven.patches.v2_6.add_unique_constraint_on_dm_channels
18+
raven.patches.v2_6.fix_duplicate_dm_channels
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import frappe
2+
3+
from raven.raven_channel_management.doctype.raven_channel_member.raven_channel_member import (
4+
on_doctype_update,
5+
)
6+
7+
8+
def execute():
9+
"""
10+
This patch adds the unique constraint to the Raven Channel Member table
11+
"""
12+
on_doctype_update()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from raven.raven_channel_management.doctype.raven_channel.raven_channel import on_doctype_update
2+
3+
4+
def execute():
5+
"""
6+
This patch adds the unique constraint to the Raven Channel table
7+
"""
8+
on_doctype_update()
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import frappe
2+
from frappe.query_builder.functions import Count
3+
4+
5+
def execute():
6+
"""
7+
8+
- Find all duplicate channel members and delete the duplicates except the oldest one
9+
- Update is_admin on oldest records where any duplicate had admin status
10+
"""
11+
12+
raven_channel_member = frappe.qb.DocType("Raven Channel Member")
13+
14+
query = (
15+
frappe.qb.from_(raven_channel_member)
16+
.select(
17+
raven_channel_member.channel_id,
18+
raven_channel_member.user_id,
19+
Count(raven_channel_member.name).as_("count"),
20+
)
21+
.groupby(raven_channel_member.channel_id, raven_channel_member.user_id)
22+
.having(Count(raven_channel_member.name) > 1)
23+
)
24+
25+
duplicate_channel_members = query.run(as_dict=True)
26+
27+
if not duplicate_channel_members:
28+
return
29+
30+
# for each duplicate channel member - we preserve the oldest one and delete the rest
31+
# if any of the duplicate channel members are admins, we preserve the oldest admin and delete the rest
32+
33+
# Update is_admin on oldest records where any duplicate had admin status
34+
# This ensures we don't lose admin privileges
35+
frappe.db.sql(
36+
"""
37+
UPDATE `tabRaven Channel Member` rcm
38+
INNER JOIN (
39+
SELECT channel_id, user_id, MIN(creation) as min_creation
40+
FROM `tabRaven Channel Member`
41+
GROUP BY channel_id, user_id
42+
HAVING COUNT(*) > 1
43+
) oldest ON rcm.channel_id = oldest.channel_id
44+
AND rcm.user_id = oldest.user_id
45+
AND rcm.creation = oldest.min_creation
46+
SET rcm.is_admin = 1
47+
WHERE EXISTS (
48+
SELECT 1 FROM `tabRaven Channel Member` dup
49+
WHERE dup.channel_id = rcm.channel_id
50+
AND dup.user_id = rcm.user_id
51+
AND dup.is_admin = 1
52+
)
53+
"""
54+
)
55+
56+
# Delete all duplicates except the oldest one
57+
frappe.db.sql(
58+
"""
59+
DELETE rcm FROM `tabRaven Channel Member` rcm
60+
INNER JOIN (
61+
SELECT channel_id, user_id, MIN(creation) as min_creation
62+
FROM `tabRaven Channel Member`
63+
GROUP BY channel_id, user_id
64+
HAVING COUNT(*) > 1
65+
) duplicates
66+
ON rcm.channel_id = duplicates.channel_id
67+
AND rcm.user_id = duplicates.user_id
68+
AND rcm.creation > duplicates.min_creation
69+
"""
70+
)
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import frappe
2+
3+
4+
def execute():
5+
"""
6+
Fix duplicate DM channels by populating dm_user fields and merging duplicates in a single pass.
7+
8+
The unique constraint on dm_user_1/dm_user_2 gets added via on_doctype_update() before patches run.
9+
If we populate dm_user fields first for ALL channels, duplicates would violate the constraint.
10+
So we must process channels one by one - either set dm_user fields OR merge into existing.
11+
12+
Logic:
13+
1. Loop through all DM channels (oldest first)
14+
2. For each channel, get canonical user pair from channel members
15+
3. If pair already processed → merge this channel into primary
16+
4. If pair is new → set dm_user fields, mark as primary
17+
"""
18+
19+
# track processed pairs: {(dm_user_1, dm_user_2): primary_channel_id}
20+
processed_pairs = {}
21+
22+
raven_channel = frappe.qb.DocType("Raven Channel")
23+
24+
# get all DM channels ordered by creation (oldest first becomes primary)
25+
dm_channels = (
26+
frappe.qb.from_(raven_channel)
27+
.select(raven_channel.name)
28+
.where(raven_channel.is_direct_message == 1)
29+
.orderby(raven_channel.creation)
30+
).run(as_dict=True)
31+
32+
for channel in dm_channels:
33+
# get canonical user pair from channel members
34+
user_pair = get_user_pair_from_members(channel.name)
35+
36+
if not user_pair:
37+
frappe.log_error(
38+
f"Could not determine user pair for DM channel {channel.name}",
39+
"Patch: fix_duplicate_dm_channels",
40+
)
41+
continue
42+
43+
dm_user_1, dm_user_2 = user_pair
44+
pair_key = (dm_user_1, dm_user_2)
45+
46+
# if the pair has already been processed, merge the channel into the primary
47+
if pair_key in processed_pairs:
48+
# duplicate - merge into primary
49+
primary_channel = processed_pairs[pair_key]
50+
merge_channel_into_primary(channel.name, primary_channel)
51+
else:
52+
# first occurrence - set dm_user fields and track as primary
53+
frappe.db.set_value(
54+
"Raven Channel",
55+
channel.name,
56+
{"dm_user_1": dm_user_1, "dm_user_2": dm_user_2},
57+
update_modified=False,
58+
)
59+
processed_pairs[pair_key] = channel.name
60+
61+
62+
def get_user_pair_from_members(channel_id: str) -> tuple | None:
63+
"""Get canonical user pair from channel members. Returns (dm_user_1, dm_user_2) or None."""
64+
users = frappe.get_all(
65+
"Raven Channel Member",
66+
filters={"channel_id": channel_id},
67+
pluck="user_id",
68+
)
69+
70+
if len(users) == 2:
71+
# Canonical order: dm_user_1 > dm_user_2 (alphabetically)
72+
if users[0] > users[1]:
73+
return (users[0], users[1])
74+
return (users[1], users[0])
75+
elif len(users) == 1:
76+
# Self message
77+
return (users[0], users[0])
78+
79+
return None
80+
81+
82+
def merge_channel_into_primary(duplicate_channel: str, primary_channel: str):
83+
"""Merge a duplicate channel into the primary channel."""
84+
85+
# Migrate all linked records
86+
migrate_linked_records(primary_channel, duplicate_channel)
87+
88+
# Migrate channel members
89+
migrate_channel_members(primary_channel, duplicate_channel)
90+
91+
# Delete the duplicate channel
92+
frappe.db.delete("Raven Channel", {"name": duplicate_channel})
93+
94+
95+
def migrate_linked_records(primary_channel: str, duplicate_channel: str):
96+
"""Move all linked records from duplicate to primary channel."""
97+
98+
linked_doctypes = [
99+
("Raven Message", "channel_id"),
100+
("Raven Message Reaction", "channel_id"),
101+
("Raven Pinned Channels", "channel_id"),
102+
("Raven Incoming Webhook", "channel_id"),
103+
("Raven Scheduler Event", "channel"),
104+
("Raven Scheduler Event", "dm"),
105+
("Raven Webhook", "channel_id"),
106+
]
107+
108+
for doctype, field in linked_doctypes:
109+
table = frappe.qb.DocType(doctype)
110+
if not table:
111+
continue
112+
113+
frappe.qb.update(table).set(field, primary_channel).where(
114+
table[field] == duplicate_channel
115+
).run()
116+
117+
118+
def migrate_channel_members(primary_channel: str, duplicate_channel: str):
119+
"""Migrate members from duplicate to primary, preserving admin status."""
120+
121+
# Preserve admin status - if user was admin in duplicate, make them admin in primary
122+
frappe.db.sql(
123+
"""
124+
UPDATE `tabRaven Channel Member`
125+
SET is_admin = 1
126+
WHERE channel_id = %s
127+
AND user_id IN (
128+
SELECT user_id FROM (
129+
SELECT user_id FROM `tabRaven Channel Member`
130+
WHERE channel_id = %s AND is_admin = 1
131+
) AS admin_users
132+
)
133+
""",
134+
(primary_channel, duplicate_channel),
135+
)
136+
137+
# Move members that don't already exist in primary
138+
frappe.db.sql(
139+
"""
140+
UPDATE `tabRaven Channel Member`
141+
SET channel_id = %s
142+
WHERE channel_id = %s
143+
AND user_id NOT IN (
144+
SELECT user_id FROM (
145+
SELECT user_id FROM `tabRaven Channel Member` WHERE channel_id = %s
146+
) AS existing_members
147+
)
148+
""",
149+
(primary_channel, duplicate_channel, primary_channel),
150+
)
151+
152+
# Delete remaining members from duplicate (they already exist in primary)
153+
frappe.db.delete("Raven Channel Member", {"channel_id": duplicate_channel})

raven/public/js/raven.bundle.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ $(document).on('app_ready', function () {
22
if (frappe.boot.show_raven_chat_on_desk && frappe.user.has_role("Raven User")) {
33

44
try {
5-
// If on mobile, do not show the chat
6-
if (frappe.is_mobile()) {
5+
// If on mobile or on frappe v16, do not show the chat
6+
if (frappe.is_mobile() || frappe.boot.versions["frappe"].startsWith('16')) {
77
return;
88
}
99
let main_section = $(document).find('.main-section');

0 commit comments

Comments
 (0)