mail/static/tests/helpers/mock_server/controllers/discuss.js

738 lines
27 KiB
JavaScript
Raw Normal View History

2024-05-03 12:40:35 +03:00
/* @odoo-module */
import { serializeDateTime } from "@web/core/l10n/dates";
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
const { DateTime } = luxon;
patch(MockServer.prototype, {
/**
* @override
*/
async performRPC(route, args) {
if (route === "/mail/attachment/upload") {
const ufile = args.body.get("ufile");
const is_pending = args.body.get("is_pending") === "true";
const model = is_pending ? "mail.compose.message" : args.body.get("thread_model");
const id = is_pending ? 0 : parseInt(args.body.get("thread_id"));
const attachmentId = this.mockCreate("ir.attachment", {
// datas,
mimetype: ufile.type,
name: ufile.name,
res_id: id,
res_model: model,
});
if (args.body.get("voice")) {
this.mockCreate("discuss.voice.metadata", { attachment_id: attachmentId });
}
return this._mockIrAttachment_attachmentFormat([attachmentId])[0];
}
return super.performRPC(...arguments);
},
/**
* @override
*/
async _performRPC(route, args) {
if (route === "/mail/attachment/delete") {
const { attachment_id } = args;
return this._mockRouteMailAttachmentRemove(attachment_id);
}
if (route === "/discuss/channel/messages") {
const { search_term, channel_id, after, around, before, limit } = args;
return this._mockRouteDiscussChannelMessages(
channel_id,
search_term,
before,
after,
around,
limit
);
}
if (route === "/discuss/channel/mute") {
const { channel_id, minutes } = args;
return this._mockRouteDiscussChannelMute(channel_id, minutes);
}
if (route === "/discuss/channel/pinned_messages") {
const { channel_id } = args;
return this._mockRouteDiscussChannelPins(channel_id);
}
if (route === "/discuss/channel/notify_typing") {
const id = args.channel_id;
const is_typing = args.is_typing;
return this._mockRouteDiscussChannelNotifyTyping(id, is_typing);
}
if (new RegExp("/discuss/channel/\\d+/partner/\\d+/avatar_128").test(route)) {
return;
}
if (route === "/discuss/channel/ping") {
return;
}
if (route === "/discuss/channel/members") {
const { channel_id, known_member_ids } = args;
return this._mockDiscussChannelloadOlderMembers([channel_id], known_member_ids);
}
if (route === "/mail/history/messages") {
const { search_term, after, before, limit } = args;
return this._mockRouteMailMessageHistory(search_term, after, before, limit);
}
if (route === "/mail/init_messaging") {
return this._mockRouteMailInitMessaging();
}
if (route === "/mail/inbox/messages") {
const { search_term, after, around, before, limit } = args;
return this._mockRouteMailMessageInbox(search_term, after, before, around, limit);
}
if (route === "/mail/link_preview") {
return this._mockRouteMailLinkPreview(args.message_id, args.clear);
}
if (route === "/mail/link_preview/delete") {
const linkPreviews = this.pyEnv["mail.link.preview"].searchRead([
["id", "in", args.link_preview_ids],
]);
for (const linkPreview of linkPreviews) {
this.pyEnv["bus.bus"]._sendone(
this._mockMailMessage__busNotificationTarget(linkPreview.message_id[0]),
"mail.link.preview/delete",
{
id: linkPreview.id,
message_id: linkPreview.message_id[0],
}
);
}
return args;
}
if (route === "/mail/load_message_failures") {
return this._mockRouteMailLoadMessageFailures();
}
if (route === "/mail/message/post") {
const finalData = {};
for (const allowedField of [
"attachment_ids",
"body",
"message_type",
"partner_ids",
"subtype_xmlid",
"parent_id",
"partner_emails",
"partner_additional_values",
]) {
if (args.post_data[allowedField] !== undefined) {
finalData[allowedField] = args.post_data[allowedField];
}
}
if (args.thread_model === "discuss.channel") {
return this._mockDiscussChannelMessagePost(args.thread_id, finalData, args.context);
}
return this._mockMailThreadMessagePost(
args.thread_model,
[args.thread_id],
finalData,
args.context
);
}
if (route === "/mail/message/reaction") {
return this._mockRouteMailMessageReaction(args);
}
if (route === "/mail/message/update_content") {
this.pyEnv["mail.message"].write([args.message_id], {
body: args.body,
attachment_ids: args.attachment_ids,
});
this.pyEnv["bus.bus"]._sendone(
this._mockMailMessage__busNotificationTarget(args.message_id),
"mail.record/insert",
{
Message: {
id: args.message_id,
body: args.body,
attachments: this._mockIrAttachment_attachmentFormat(args.attachment_ids),
},
}
);
return this._mockMailMessageMessageFormat([args.message_id])[0];
}
if (route === "/mail/partner/from_email") {
return this._mockRouteMailPartnerFromEmail(args.emails, args.additional_values);
}
if (route === "/mail/read_subscription_data") {
const follower_id = args.follower_id;
return this._mockRouteMailReadSubscriptionData(follower_id);
}
if (route === "/mail/rtc/channel/join_call") {
return this._mockRouteMailRtcChannelJoinCall(
args.channel_id,
args.check_rtc_session_ids
);
}
if (route === "/mail/rtc/channel/leave_call") {
return this._mockRouteMailRtcChannelLeaveCall(args.channel_id);
}
if (route === "/mail/rtc/session/update_and_broadcast") {
return this._mockRouteMailRtcSessionUpdateAndBroadcast(args.session_id, args.values);
}
if (route === "/mail/starred/messages") {
const { search_term, after, before, limit } = args;
return this._mockRouteMailMessageStarredMessages(search_term, after, before, limit);
}
if (route === "/mail/thread/data") {
return this._mockRouteMailThreadData(
args.thread_model,
args.thread_id,
args.request_list
);
}
if (route === "/mail/thread/messages") {
const { search_term, after, around, before, limit, thread_model, thread_id } = args;
return this._mockRouteMailThreadFetchMessages(
thread_model,
thread_id,
search_term,
before,
after,
around,
limit
);
}
if (route === "/discuss/gif/favorites") {
return [[]];
}
return super._performRPC(route, args);
},
/**
* Simulates the `/discuss/channel/pinned_messages` route.
*/
_mockRouteDiscussChannelPins(channel_id) {
const messageIds = this.pyEnv["mail.message"].search([
["model", "=", "discuss.channel"],
["res_id", "=", channel_id],
["pinned_at", "!=", false],
]);
return this._mockMailMessageMessageFormat(messageIds);
},
/**
* Simulates the `/mail/init_messaging` route.
*
* @private
* @returns {Object}
*/
_mockRouteMailInitMessaging() {
if (this._mockMailGuest__getGuestFromContext() && this.pyEnv.currentUser?._is_public()) {
return this._mockMailGuest__initMessaging();
}
return this._mockResUsers_InitMessaging([this.pyEnv.currentUserId]);
},
/**
* Simulates the `/mail/attachment/delete` route.
*
* @private
* @param {integer} attachment_id
*/
async _mockRouteMailAttachmentRemove(attachment_id) {
this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "ir.attachment/delete", {
id: attachment_id,
});
return this.pyEnv["ir.attachment"].unlink([attachment_id]);
},
/**
* Simulates the `/discuss/channel/messages` route.
*
* @private
* @param {integer} channel_id
* @param {integer} limit
* @param {integer} before
* @param {integer} after
* @param {integer} around
* @returns {Object} list of messages
*/
async _mockRouteDiscussChannelMessages(
channel_id,
search_term = false,
before = false,
after = false,
around = false,
limit = 30
) {
const domain = [
["res_id", "=", channel_id],
["model", "=", "discuss.channel"],
["message_type", "!=", "user_notification"],
];
const res = this._mockMailMessage_MessageFetch(
domain,
search_term,
before,
after,
around,
limit
);
if (!around) {
this._mockMailMessageSetMessageDone(res.messages.map((message) => message.id));
}
return {
...res,
messages: this._mockMailMessageMessageFormat(res.messages.map((message) => message.id)),
};
},
/**
* Simulates the `/discuss/channel/mute` route.
*
* @private
* @param {integer} channel_id
* @param {integer} minutes
*/
_mockRouteDiscussChannelMute(channel_id, minutes) {
const member = this._mockDiscussChannelMember__getAsSudoFromContext(channel_id);
let mute_until_dt;
if (minutes === -1) {
mute_until_dt = serializeDateTime(DateTime.fromISO("9999-12-31T23:59:59"));
} else if (minutes) {
mute_until_dt = serializeDateTime(DateTime.now().plus({ minutes }));
} else {
mute_until_dt = false;
}
this.pyEnv["discuss.channel.member"].write([member.id], { mute_until_dt });
const channel_data = {
id: member.channel_id[0],
model: "discuss.channel",
mute_until_dt,
};
this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.record/insert", {
Thread: channel_data,
});
return "dummy";
},
/**
* Simulates the `/discuss/channel/notify_typing` route.
*
* @private
* @param {integer} channel_id
* @param {integer} limit
* @param {Object} [context={}]
*/
async _mockRouteDiscussChannelNotifyTyping(channel_id, is_typing) {
const memberOfCurrentUser =
this._mockDiscussChannelMember__getAsSudoFromContext(channel_id);
if (!memberOfCurrentUser) {
return;
}
this._mockDiscussChannelMember_NotifyTyping([memberOfCurrentUser.id], is_typing);
},
/**
* Simulates the `/mail/load_message_failures` route.
*
* @private
* @returns {Object[]}
*/
_mockRouteMailLoadMessageFailures() {
return this._mockResPartner_MessageFetchFailed(this.pyEnv.currentPartnerId);
},
/**
* Simulates `/mail/link_preview` route.
*
* @private
* @param {integer} message_id
* @returns {Object}
*/
_mockRouteMailLinkPreview(message_id, clear = false) {
const linkPreviews = [];
const [message] = this.pyEnv["mail.message"].searchRead([["id", "=", message_id]]);
if (message.body.includes("https://make-link-preview.com")) {
if (clear) {
const [linkPreview] = this.pyEnv["mail.link.preview"].searchRead([
["message_id", "=", message_id],
]);
this.pyEnv["bus.bus"]._sendone(
this._mockMailMessage__busNotificationTarget(linkPreview.message_id),
"mail.link.preview/delete",
{
id: linkPreview.id,
message_id: linkPreview.message_id,
}
);
}
const linkPreviewId = this.pyEnv["mail.link.preview"].create({
message_id: message.id,
og_description: "test description",
og_title: "Article title",
og_type: "article",
source_url: "https://make-link-preview.com",
});
const [linkPreview] = this.pyEnv["mail.link.preview"].searchRead([
["id", "=", linkPreviewId],
]);
linkPreviews.push(this._mockMailLinkPreviewFormat(linkPreview));
this.pyEnv["bus.bus"]._sendone(
this._mockMailMessage__busNotificationTarget(message_id),
"mail.record/insert",
{ LinkPreview: linkPreviews }
);
}
},
/**
* Simulates `/mail/message/reaction` route.
*/
_mockRouteMailMessageReaction({ action, content, message_id }) {
return this._mockMailMessage_messageReaction(message_id, content, action);
},
/**
* Simulates the `/mail/history/messages` route.
*
* @private
* @returns {Object}
*/
_mockRouteMailMessageHistory(search_term = false, after = false, before = false, limit = 30) {
const domain = [["needaction", "=", false]];
const res = this._mockMailMessage_MessageFetch(
domain,
search_term,
before,
after,
false,
limit
);
const messagesWithNotification = res.messages.filter((message) => {
const notifs = this.pyEnv["mail.notification"].searchRead([
["mail_message_id", "=", message.id],
["is_read", "=", true],
["res_partner_id", "=", this.pyEnv.currentPartnerId],
]);
return notifs.length > 0;
});
return {
...res,
messages: this._mockMailMessageMessageFormat(
messagesWithNotification.map((message) => message.id)
),
};
},
/**
* Simulates the `/mail/inbox/messages` route.
*
* @private
* @returns {Object}
*/
_mockRouteMailMessageInbox(
search_term = false,
after = false,
before = false,
around = false,
limit = 30
) {
const domain = [["needaction", "=", true]];
const res = this._mockMailMessage_MessageFetch(
domain,
search_term,
before,
after,
around,
limit
);
return {
...res,
messages: this._mockMailMessageFormatPersonalize(
res.messages.map((message) => message.id)
),
};
},
/**
* Simulates the `/mail/starred/messages` route.
*
* @private
* @returns {Object}
*/
_mockRouteMailMessageStarredMessages(
search_term = false,
after = false,
before = false,
limit = 30
) {
const domain = [["starred_partner_ids", "in", [this.pyEnv.currentPartnerId]]];
const res = this._mockMailMessage_MessageFetch(
domain,
search_term,
before,
after,
false,
limit
);
return {
...res,
messages: this._mockMailMessageMessageFormat(res.messages.map((message) => message.id)),
};
},
/**
* Simulates the `/mail/partner/from_email` route.
*
* @private
* @param {string[]} emails
* @returns {Object[]} list of partner data
*/
_mockRouteMailPartnerFromEmail(emails, additional_values = {}) {
const partners = emails.map(
(email) => this.pyEnv["res.partner"].search([["email", "=", email]])[0]
);
for (const index in partners) {
if (!partners[index]) {
const email = emails[index];
partners[index] = this.pyEnv["res.partner"].create({
email,
name: email,
...(additional_values[email] || {}),
});
}
}
return partners.map((partner_id) => {
const partner = this.getRecords("res.partner", [["id", "=", partner_id]])[0];
return { id: partner_id, name: partner.name, email: partner.email };
});
},
/**
* Simulates the `/mail/read_subscription_data` route.
*
* @private
* @param {integer} follower_id
* @returns {Object[]} list of followed subtypes
*/
async _mockRouteMailReadSubscriptionData(follower_id) {
const follower = this.getRecords("mail.followers", [["id", "=", follower_id]])[0];
const subtypes = this.getRecords("mail.message.subtype", [
"&",
["hidden", "=", false],
"|",
["res_model", "=", follower.res_model],
["res_model", "=", false],
]);
const subtypes_list = subtypes.map((subtype) => {
const parent = this.getRecords("mail.message.subtype", [
["id", "=", subtype.parent_id],
])[0];
return {
default: subtype.default,
followed: follower.subtype_ids.includes(subtype.id),
id: subtype.id,
internal: subtype.internal,
name: subtype.name,
parent_model: parent ? parent.res_model : false,
res_model: subtype.res_model,
sequence: subtype.sequence,
};
});
// NOTE: server is also doing a sort here, not reproduced for simplicity
return subtypes_list;
},
/**
* Simulates the `/mail/rtc/channel/join_call` route.
*
* @private
* @param {integer} channel_id
* @returns {integer[]} [check_rtc_session_ids]
*/
async _mockRouteMailRtcChannelJoinCall(channel_id, check_rtc_session_ids = []) {
const memberOfCurrentUser =
this._mockDiscussChannelMember__getAsSudoFromContext(channel_id);
const sessionId = this.pyEnv["discuss.channel.rtc.session"].create({
channel_member_id: memberOfCurrentUser.id,
channel_id, // on the server, this is a related field from channel_member_id and not explicitly set
guest_id: memberOfCurrentUser.guest_id[0],
partner_id: memberOfCurrentUser.partner_id[0],
});
const channelMembers = this.getRecords("discuss.channel.member", [
["channel_id", "=", channel_id],
]);
const rtcSessions = this.getRecords("discuss.channel.rtc.session", [
["channel_member_id", "in", channelMembers.map((channelMember) => channelMember.id)],
]);
return {
iceServers: false,
rtcSessions: [
[
"ADD",
rtcSessions.map((rtcSession) =>
this._mockDiscussChannelRtcSession_DiscussChannelRtcSessionFormat(
rtcSession.id
)
),
],
],
sessionId: sessionId,
};
},
/**
* Simulates the `/mail/rtc/channel/leave_call` route.
*
* @private
* @param {integer} channelId
*/
async _mockRouteMailRtcChannelLeaveCall(channel_id) {
const channelMembers = this.getRecords("discuss.channel.member", [
["channel_id", "=", channel_id],
]);
const rtcSessions = this.getRecords("discuss.channel.rtc.session", [
["channel_member_id", "in", channelMembers.map((channelMember) => channelMember.id)],
]);
const notifications = [];
const channelInfo =
this._mockDiscussChannelRtcSession_DiscussChannelRtcSessionFormatByChannel(
rtcSessions.map((rtcSession) => rtcSession.id)
);
for (const [channelId, sessionsData] of Object.entries(channelInfo)) {
const channel = this.pyEnv["discuss.channel"].searchRead([
["id", "=", parseInt(channelId)],
])[0];
const notificationRtcSessions = sessionsData.map((sessionsDataPoint) => {
return { id: sessionsDataPoint.id };
});
notifications.push([
channel,
"discuss.channel/rtc_sessions_update",
{
id: Number(channelId), // JS object keys are strings, but the type from the server is number
rtcSessions: [["DELETE", notificationRtcSessions]],
},
]);
}
for (const rtcSession of rtcSessions) {
const target = rtcSession.guest_id
? this.pyEnv["mail.guest"].searchRead([["id", "=", rtcSession.guest_id]])[0]
: this.pyEnv["res.partner"].searchRead([["id", "=", rtcSession.partner_id]])[0];
notifications.push([
target,
"discuss.channel.rtc.session/ended",
{ sessionId: rtcSession.id },
]);
}
this.pyEnv["bus.bus"]._sendmany(notifications);
},
/**
* Simulates the `/mail/rtc/session/update_and_broadcast` route.
*
* @param {number} session_id
* @param {object} values
*/
async _mockRouteMailRtcSessionUpdateAndBroadcast(session_id, values) {
const [session] = this.pyEnv["discuss.channel.rtc.session"].searchRead([
["id", "=", session_id],
]);
const [currentChannelMember] = this.pyEnv["discuss.channel.member"].searchRead([
["id", "=", session.channel_member_id[0]],
]);
if (session && currentChannelMember.partner_id[0] === this.pyEnv.currentPartnerId) {
this._mockDiscussChannelRtcSession__updateAndBroadcast(session.id, values);
}
},
/**
* Simulates the `/mail/thread/data` route.
*
* @param {string} thread_model
* @param {integer} thread_id
* @param {string[]} request_list
* @returns {Object}
*/
async _mockRouteMailThreadData(thread_model, thread_id, request_list) {
const res = {
hasWriteAccess: true, // mimic user with write access by default
hasReadAccess: true,
};
const thread = this.pyEnv[thread_model].searchRead([["id", "=", thread_id]])[0];
if (!thread) {
res["hasReadAccess"] = false;
return res;
}
res["canPostOnReadonly"] = thread_model === "discuss.channel"; // model that have attr _mail_post_access='read'
if (request_list.includes("activities")) {
const activities = this.pyEnv["mail.activity"].searchRead([
["id", "in", thread.activity_ids || []],
]);
res["activities"] = this._mockMailActivityActivityFormat(
activities.map((activity) => activity.id)
);
}
if (request_list.includes("attachments")) {
const attachments = this.pyEnv["ir.attachment"].searchRead([
["res_id", "=", thread.id],
["res_model", "=", thread_model],
]); // order not done for simplicity
res["attachments"] = this._mockIrAttachment_attachmentFormat(
attachments.map((attachment) => attachment.id)
);
// Specific implementation of mail.thread.main.attachment
if (this.models[thread_model].fields["message_main_attachment_id"]) {
res["mainAttachment"] = thread.message_main_attachment_id
? { id: thread.message_main_attachment_id[0] }
: false;
}
}
if (request_list.includes("followers")) {
const domain = [
["res_id", "=", thread.id],
["res_model", "=", thread_model],
];
res["followersCount"] = (thread.message_follower_ids || []).length;
const selfFollower = this.pyEnv["mail.followers"].searchRead(
domain.concat([["partner_id", "=", this.pyEnv.currentPartnerId]])
)[0];
res["selfFollower"] = selfFollower
? this._mockMailFollowers_FormatForChatter(selfFollower.id)[0]
: false;
res["followers"] = this._mockMailThreadMessageGetFollowers(thread_model, [thread_id]);
res["recipientsCount"] = (thread.message_follower_ids || []).length - 1;
res["recipients"] = this._mockMailThreadMessageGetFollowers(
thread_model,
[thread_id],
undefined,
100,
{ filter_recipients: true }
);
}
if (request_list.includes("suggestedRecipients")) {
res["suggestedRecipients"] = this._mockMailThread_MessageGetSuggestedRecipients(
thread_model,
[thread.id]
)[thread_id];
}
return res;
},
/**
* Simulates the `/mail/thread/messages` route.
*
* @private
* @param {string} res_model
* @param {integer} res_id
* @param {integer} before
* @param {integer} after
* @param {integer} limit
* @returns {Object[]} list of messages
*/
async _mockRouteMailThreadFetchMessages(
res_model,
res_id,
search_term = false,
before = false,
after = false,
around = false,
limit = 30
) {
const domain = [
["res_id", "=", res_id],
["model", "=", res_model],
["message_type", "!=", "user_notification"],
];
const res = this._mockMailMessage_MessageFetch(
domain,
search_term,
before,
after,
around,
limit
);
this._mockMailMessageSetMessageDone(res.messages.map((message) => message.id));
return {
...res,
messages: this._mockMailMessageMessageFormat(res.messages.map((message) => message.id)),
};
},
});