1322 lines
50 KiB
JavaScript
1322 lines
50 KiB
JavaScript
import {
|
|
authenticate,
|
|
getKwArgs,
|
|
logout,
|
|
makeKwArgs,
|
|
MockServer,
|
|
MockServerError,
|
|
models,
|
|
serverState,
|
|
unmakeKwArgs,
|
|
} from "@web/../tests/web_test_helpers";
|
|
import { serializeDateTime } from "@web/core/l10n/dates";
|
|
import { registry } from "@web/core/registry";
|
|
import { groupBy } from "@web/core/utils/arrays";
|
|
|
|
export const DISCUSS_ACTION_ID = 104;
|
|
|
|
/**
|
|
* @template [T={}]
|
|
* @typedef {import("@web/../tests/web_test_helpers").RouteCallback<T>} RouteCallback
|
|
*/
|
|
|
|
const { DateTime } = luxon;
|
|
|
|
/** @param {import("./mock_model").MailGuest} guest */
|
|
export const authenticateGuest = (guest) => {
|
|
const { env } = MockServer;
|
|
/** @type {import("mock_models").ResUsers} */
|
|
const ResUsers = env["res.users"];
|
|
if (!guest?.id) {
|
|
throw new MockServerError("Unauthorized");
|
|
}
|
|
const [publicUser] = ResUsers.read(serverState.publicUserId);
|
|
env.cookie.set("dgid", guest.id);
|
|
authenticate(publicUser.login, publicUser.password);
|
|
env.uid = serverState.publicUserId;
|
|
};
|
|
|
|
/**
|
|
* Executes the given callback as the given guest, then restores the previous user.
|
|
*
|
|
* @param {number} guestId
|
|
* @param {() => any} fn
|
|
*/
|
|
export async function withGuest(guestId, fn) {
|
|
const { env } = MockServer;
|
|
/** @type {import("mock_models").MailGuest} */
|
|
const MailGuest = env["mail.guest"];
|
|
const currentUser = env.user;
|
|
const [targetGuest] = MailGuest.browse(guestId);
|
|
authenticateGuest(targetGuest);
|
|
let result;
|
|
try {
|
|
result = await fn();
|
|
} finally {
|
|
if (currentUser) {
|
|
authenticate(currentUser.login, currentUser.password);
|
|
} else {
|
|
logout();
|
|
env.cookie.delete("dgid");
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** @param {Request} request */
|
|
export const parseRequestParams = async (request) => {
|
|
const response = await request.json();
|
|
return response.params;
|
|
};
|
|
|
|
const onRpcBeforeGlobal = { cb: (route, args) => {} };
|
|
const onRpcAfterGlobal = { cb: (route, args) => {} };
|
|
// using a registry category to not expose for manual import
|
|
// We should use `onRpcBefore`/`onRpcAfter` with 1st parameter being (route, args) callback function
|
|
registry.category("mail.on_rpc_before_global").add(true, onRpcBeforeGlobal);
|
|
registry.category("mail.on_rpc_after_global").add(true, onRpcAfterGlobal);
|
|
export function registerRoute(route, handler) {
|
|
const beforeCallableHandler = async function (request) {
|
|
let args;
|
|
try {
|
|
args = await parseRequestParams(request);
|
|
} catch {
|
|
args = await request.text();
|
|
}
|
|
let res = await onRpcBeforeGlobal.cb?.(route, args);
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
res = await beforeCallableHandler.before?.(args);
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
const response = handler.call(this, request);
|
|
res = await beforeCallableHandler.after?.(response);
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
return response;
|
|
};
|
|
registry.category("mock_rpc").add(route, beforeCallableHandler);
|
|
}
|
|
|
|
// RPC handlers
|
|
|
|
registerRoute("/mail/attachment/upload", mail_attachment_upload);
|
|
/** @type {RouteCallback}} */
|
|
async function mail_attachment_upload(request) {
|
|
/** @type {import("mock_models").DiscussVoiceMetadata} */
|
|
const DiscussVoiceMetadata = this.env["discuss.voice.metadata"];
|
|
/** @type {import("mock_models").IrAttachment} */
|
|
const IrAttachment = this.env["ir.attachment"];
|
|
|
|
const body = await request.text();
|
|
const ufile = body.get("ufile");
|
|
const is_pending = body.get("is_pending") === "true";
|
|
const model = is_pending ? "mail.compose.message" : body.get("thread_model");
|
|
const id = is_pending ? 0 : parseInt(body.get("thread_id"));
|
|
const attachmentId = IrAttachment.create({
|
|
// datas,
|
|
mimetype: ufile.type,
|
|
name: ufile.name,
|
|
res_id: id,
|
|
res_model: model,
|
|
});
|
|
if (body.get("voice")) {
|
|
DiscussVoiceMetadata.create({ attachment_id: attachmentId });
|
|
}
|
|
return {
|
|
data: new mailDataHelpers.Store(IrAttachment.browse(attachmentId)).get_result(),
|
|
};
|
|
}
|
|
|
|
registerRoute("/mail/attachment/delete", mail_attachment_delete);
|
|
/** @type {RouteCallback} */
|
|
async function mail_attachment_delete(request) {
|
|
/** @type {import("mock_models").BusBus} */
|
|
const BusBus = this.env["bus.bus"];
|
|
/** @type {import("mock_models").IrAttachment} */
|
|
const IrAttachment = this.env["ir.attachment"];
|
|
/** @type {import("mock_models").ResPartner} */
|
|
const ResPartner = this.env["res.partner"];
|
|
|
|
const { attachment_id } = await parseRequestParams(request);
|
|
const [partner] = ResPartner.read(this.env.user.partner_id);
|
|
BusBus._sendone(partner, "ir.attachment/delete", {
|
|
id: attachment_id,
|
|
});
|
|
return IrAttachment.unlink([attachment_id]);
|
|
}
|
|
|
|
registerRoute("/discuss/channel/attachments", load_attachments);
|
|
/** @type {RouteCallback} */
|
|
async function load_attachments(request) {
|
|
/** @type {import("mock_models").IrAttachment} */
|
|
const IrAttachment = this.env["ir.attachment"];
|
|
|
|
const {
|
|
channel_id,
|
|
limit = 30,
|
|
older_attachment_id = null,
|
|
} = await parseRequestParams(request);
|
|
const attachmentIds = IrAttachment.filter(
|
|
({ id, res_id, res_model }) =>
|
|
res_id === channel_id &&
|
|
res_model === "discuss.channel" &&
|
|
(!older_attachment_id || id < older_attachment_id)
|
|
)
|
|
.sort()
|
|
.slice(0, limit)
|
|
.map(({ id }) => id);
|
|
return new mailDataHelpers.Store(IrAttachment.browse(attachmentIds)).get_result();
|
|
}
|
|
|
|
registerRoute("/mail/rtc/channel/join_call", channel_call_join);
|
|
/** @type {RouteCallback} */
|
|
async function channel_call_join(request) {
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
/** @type {import("mock_models").DiscussChannelMember} */
|
|
const DiscussChannelMember = this.env["discuss.channel.member"];
|
|
/** @type {import("mock_models").DiscussChannelRtcSession} */
|
|
const DiscussChannelRtcSession = this.env["discuss.channel.rtc.session"];
|
|
|
|
const { channel_id } = await parseRequestParams(request);
|
|
const memberOfCurrentUser = DiscussChannel._find_or_create_member_for_self(channel_id);
|
|
const sessionId = DiscussChannelRtcSession.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,
|
|
partner_id: memberOfCurrentUser.partner_id,
|
|
});
|
|
const channelMembers = DiscussChannelMember._filter([["channel_id", "=", channel_id]]);
|
|
const rtcSessions = DiscussChannelRtcSession._filter([
|
|
["channel_member_id", "in", channelMembers.map((channelMember) => channelMember.id)],
|
|
]);
|
|
return new mailDataHelpers.Store(DiscussChannel.browse(channel_id), {
|
|
rtcSessions: mailDataHelpers.Store.many(rtcSessions, "ADD"),
|
|
})
|
|
.add("Rtc", {
|
|
iceServers: false,
|
|
selfSession: mailDataHelpers.Store.one(DiscussChannelRtcSession.browse(sessionId)),
|
|
})
|
|
.get_result();
|
|
}
|
|
|
|
registerRoute("/mail/rtc/channel/leave_call", channel_call_leave);
|
|
/** @type {RouteCallback} */
|
|
async function channel_call_leave(request) {
|
|
/** @type {import("mock_models").BusBus} */
|
|
const BusBus = this.env["bus.bus"];
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
/** @type {import("mock_models").DiscussChannelMember} */
|
|
const DiscussChannelMember = this.env["discuss.channel.member"];
|
|
/** @type {import("mock_models").DiscussChannelRtcSession} */
|
|
const DiscussChannelRtcSession = this.env["discuss.channel.rtc.session"];
|
|
/** @type {import("mock_models").MailGuest} */
|
|
const MailGuest = this.env["mail.guest"];
|
|
/** @type {import("mock_models").ResPartner} */
|
|
const ResPartner = this.env["res.partner"];
|
|
|
|
const { channel_id } = await parseRequestParams(request);
|
|
const channelMembers = DiscussChannelMember._filter([["channel_id", "=", channel_id]]);
|
|
const rtcSessions = DiscussChannelRtcSession._filter([
|
|
["channel_member_id", "in", channelMembers.map((channelMember) => channelMember.id)],
|
|
]);
|
|
const notifications = [];
|
|
const sessionsByChannelId = {};
|
|
for (const session of rtcSessions) {
|
|
const [member] = DiscussChannelMember.browse(session.channel_member_id);
|
|
if (!sessionsByChannelId[member.channel_id]) {
|
|
sessionsByChannelId[member.channel_id] = [];
|
|
}
|
|
sessionsByChannelId[member.channel_id].push(session);
|
|
}
|
|
for (const [channelId, sessions] of Object.entries(sessionsByChannelId)) {
|
|
const channel = DiscussChannel.search_read([["id", "=", parseInt(channelId)]])[0];
|
|
notifications.push([
|
|
channel,
|
|
"mail.record/insert",
|
|
new mailDataHelpers.Store(DiscussChannel.browse(Number(channelId)), {
|
|
rtcSessions: mailDataHelpers.Store.many(
|
|
DiscussChannelRtcSession.browse(sessions.map((session) => session.id)),
|
|
"DELETE",
|
|
makeKwArgs({ only_id: true })
|
|
),
|
|
}).get_result(),
|
|
]);
|
|
}
|
|
for (const rtcSession of rtcSessions) {
|
|
const target = rtcSession.guest_id
|
|
? MailGuest.search_read([["id", "=", rtcSession.guest_id]])[0]
|
|
: ResPartner.search_read([["id", "=", rtcSession.partner_id]])[0];
|
|
notifications.push([
|
|
target,
|
|
"discuss.channel.rtc.session/ended",
|
|
{ sessionId: rtcSession.id },
|
|
]);
|
|
}
|
|
BusBus._sendmany(notifications);
|
|
}
|
|
|
|
registerRoute("/discuss/channel/fold", discuss_channel_fold);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_channel_fold(request) {
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
/** @type {import("mock_models").DiscussChannelMember} */
|
|
const DiscussChannelMember = this.env["discuss.channel.member"];
|
|
|
|
const { channel_id, state, state_count } = await parseRequestParams(request);
|
|
const memberOfCurrentUser = DiscussChannel._find_or_create_member_for_self(channel_id);
|
|
return DiscussChannelMember._channel_fold(memberOfCurrentUser.id, state, state_count);
|
|
}
|
|
|
|
registerRoute("/discuss/channel/info", discuss_channel_info);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_channel_info(request) {
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
|
|
const { channel_id } = await parseRequestParams(request);
|
|
const channel = DiscussChannel.search([["id", "=", channel_id]]);
|
|
if (!channel.length) {
|
|
return;
|
|
}
|
|
return new mailDataHelpers.Store(channel).get_result();
|
|
}
|
|
|
|
registerRoute("/discuss/channel/members", discuss_channel_members);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_channel_members(request) {
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
|
|
const { channel_id, known_member_ids } = await parseRequestParams(request);
|
|
return DiscussChannel._load_more_members([channel_id], known_member_ids);
|
|
}
|
|
|
|
registerRoute("/discuss/channel/messages", discuss_channel_messages);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_channel_messages(request) {
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
|
|
const {
|
|
after,
|
|
around,
|
|
before,
|
|
channel_id,
|
|
limit = 30,
|
|
search_term,
|
|
} = await parseRequestParams(request);
|
|
const domain = [
|
|
["res_id", "=", channel_id],
|
|
["model", "=", "discuss.channel"],
|
|
["message_type", "!=", "user_notification"],
|
|
];
|
|
const res = MailMessage._message_fetch(domain, search_term, before, after, around, limit);
|
|
const { messages } = res;
|
|
delete res.messages;
|
|
if (!around) {
|
|
MailMessage.set_message_done(messages.map((message) => message.id));
|
|
}
|
|
return {
|
|
...res,
|
|
data: new mailDataHelpers.Store(
|
|
MailMessage.browse(messages.map((message) => message.id)),
|
|
makeKwArgs({ for_current_user: true })
|
|
).get_result(),
|
|
messages: mailDataHelpers.Store.many_ids(messages),
|
|
};
|
|
}
|
|
|
|
registerRoute("/discuss/channel/sub_channel/create", discuss_channel_sub_channel_create);
|
|
async function discuss_channel_sub_channel_create(request) {
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
const { from_message_id, parent_channel_id, name } = await parseRequestParams(request);
|
|
return DiscussChannel._create_sub_channel(
|
|
[parent_channel_id],
|
|
makeKwArgs({ from_message_id, name })
|
|
);
|
|
}
|
|
|
|
registerRoute("/discuss/channel/sub_channel/fetch", discuss_channel_sub_channel_fetch);
|
|
async function discuss_channel_sub_channel_fetch(request) {
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
const { parent_channel_id, before, limit } = await parseRequestParams(request);
|
|
const domain = [["parent_channel_id", "=", parent_channel_id]];
|
|
if (before) {
|
|
domain.push(["id", "<", before]);
|
|
}
|
|
const subChannels = DiscussChannel.search(domain, makeKwArgs({ limit, order: "id DESC" }));
|
|
const store = new mailDataHelpers.Store(subChannels);
|
|
const lastMessageIds = [];
|
|
for (const channel of subChannels) {
|
|
const lastMessageId = Math.max(channel.message_ids);
|
|
if (lastMessageId) {
|
|
lastMessageIds.push(lastMessageId);
|
|
}
|
|
}
|
|
store.add(MailMessage.browse(lastMessageIds));
|
|
return store.get_result();
|
|
}
|
|
|
|
registerRoute("/discuss/settings/mute", discuss_settings_mute);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_settings_mute(request) {
|
|
/** @type {import("mock_models").BusBus} */
|
|
const BusBus = this.env["bus.bus"];
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
/** @type {import("mock_models").ResUsersSettings} */
|
|
const ResUsersSettings = this.env["res.users.settings"];
|
|
/** @type {import("mock_models").DiscussChannelMember} */
|
|
const DiscussChannelMember = this.env["discuss.channel.member"];
|
|
/** @type {import("mock_models").ResPartner} */
|
|
const ResPartner = this.env["res.partner"];
|
|
|
|
const { channel_id, minutes } = await parseRequestParams(request);
|
|
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;
|
|
}
|
|
if (channel_id) {
|
|
const member = DiscussChannel._find_or_create_member_for_self(channel_id);
|
|
DiscussChannelMember.write([member.id], { mute_until_dt });
|
|
const [partner] = ResPartner.read(this.env.user.partner_id);
|
|
BusBus._sendone(
|
|
partner,
|
|
"mail.record/insert",
|
|
new mailDataHelpers.Store(DiscussChannel.browse(member.channel_id), {
|
|
mute_until_dt,
|
|
}).get_result()
|
|
);
|
|
} else {
|
|
const settings = ResUsersSettings._find_or_create_for_user(this.env.user.id);
|
|
ResUsersSettings.set_res_users_settings(settings.id, { mute_until_dt });
|
|
}
|
|
return "dummy";
|
|
}
|
|
|
|
registerRoute("/discuss/channel/notify_typing", discuss_channel_notify_typing);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_channel_notify_typing(request) {
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
/** @type {import("mock_models").DiscussChannelMember} */
|
|
const DiscussChannelMember = this.env["discuss.channel.member"];
|
|
|
|
const { channel_id, is_typing } = await parseRequestParams(request);
|
|
const memberOfCurrentUser = DiscussChannel._find_or_create_member_for_self(channel_id);
|
|
if (!memberOfCurrentUser) {
|
|
return;
|
|
}
|
|
DiscussChannelMember.notify_typing([memberOfCurrentUser.id], is_typing);
|
|
}
|
|
|
|
registerRoute("/discuss/channel/ping", channel_ping);
|
|
/** @type {RouteCallback} */
|
|
async function channel_ping(request) {}
|
|
|
|
registerRoute("/discuss/channel/pinned_messages", discuss_channel_pins);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_channel_pins(request) {
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
|
|
const { channel_id } = await parseRequestParams(request);
|
|
const messageIds = MailMessage.search([
|
|
["model", "=", "discuss.channel"],
|
|
["res_id", "=", channel_id],
|
|
["pinned_at", "!=", false],
|
|
]);
|
|
return new mailDataHelpers.Store(
|
|
messageIds,
|
|
makeKwArgs({ for_current_user: true })
|
|
).get_result();
|
|
}
|
|
|
|
registerRoute("/discuss/channel/mark_as_read", discuss_channel_mark_as_read);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_channel_mark_as_read(request) {
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannelMember = this.env["discuss.channel.member"];
|
|
const { channel_id, last_message_id, sync } = await parseRequestParams(request);
|
|
const [partner, guest] = this.env["res.partner"]._get_current_persona();
|
|
const [memberId] = this.env["discuss.channel.member"].search([
|
|
["channel_id", "=", channel_id],
|
|
partner ? ["partner_id", "=", partner.id] : ["guest_id", "=", guest.id],
|
|
]);
|
|
if (!memberId) {
|
|
return; // ignore if the member left in the meantime
|
|
}
|
|
return DiscussChannelMember._mark_as_read([memberId], last_message_id, sync);
|
|
}
|
|
|
|
registerRoute("/discuss/channel/mark_as_unread", discuss_channel_mark_as_unread);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_channel_mark_as_unread(request) {
|
|
const { channel_id, message_id } = await parseRequestParams(request);
|
|
const [partner, guest] = this.env["res.partner"]._get_current_persona();
|
|
const [memberId] = this.env["discuss.channel.member"].search([
|
|
["channel_id", "=", channel_id],
|
|
partner ? ["partner_id", "=", partner.id] : ["guest_id", "=", guest.id],
|
|
]);
|
|
return this.env["discuss.channel.member"]._set_new_message_separator(
|
|
[memberId],
|
|
message_id,
|
|
true
|
|
);
|
|
}
|
|
|
|
registerRoute("/discuss/gif/favorites", get_favorites);
|
|
/** @type {RouteCallback} */
|
|
async function get_favorites(request) {
|
|
return [[]];
|
|
}
|
|
|
|
registerRoute("/mail/history/messages", discuss_history_messages);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_history_messages(request) {
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
/** @type {import("mock_models").MailNotification} */
|
|
const MailNotification = this.env["mail.notification"];
|
|
|
|
const { after, around, before, limit = 30, search_term } = await parseRequestParams(request);
|
|
const domain = [["needaction", "=", false]];
|
|
const res = MailMessage._message_fetch(domain, search_term, before, after, around, limit);
|
|
const { messages } = res;
|
|
delete res.messages;
|
|
const messagesWithNotification = messages.filter((message) => {
|
|
const notifs = MailNotification.search_read([
|
|
["mail_message_id", "=", message.id],
|
|
["is_read", "=", true],
|
|
["res_partner_id", "=", this.env.user.partner_id],
|
|
]);
|
|
return notifs.length > 0;
|
|
});
|
|
return {
|
|
...res,
|
|
data: new mailDataHelpers.Store(
|
|
MailMessage.browse(messagesWithNotification.map((message) => message.id)),
|
|
makeKwArgs({ for_current_user: true })
|
|
).get_result(),
|
|
messages: mailDataHelpers.Store.many_ids(messages),
|
|
};
|
|
}
|
|
|
|
registerRoute("/mail/inbox/messages", discuss_inbox_messages);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_inbox_messages(request) {
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
|
|
const { after, around, before, limit = 30, search_term } = await parseRequestParams(request);
|
|
const domain = [["needaction", "=", true]];
|
|
const res = MailMessage._message_fetch(domain, search_term, before, after, around, limit);
|
|
const { messages } = res;
|
|
delete res.messages;
|
|
return {
|
|
...res,
|
|
data: new mailDataHelpers.Store(
|
|
MailMessage.browse(messages.map((message) => message.id)),
|
|
makeKwArgs({ for_current_user: true, add_followers: true })
|
|
).get_result(),
|
|
messages: mailDataHelpers.Store.many_ids(messages),
|
|
};
|
|
}
|
|
|
|
registerRoute("/mail/link_preview", mail_link_preview);
|
|
/** @type {RouteCallback} */
|
|
async function mail_link_preview(request) {
|
|
/** @type {import("mock_models").BusBus} */
|
|
const BusBus = this.env["bus.bus"];
|
|
/** @type {import("mock_models").MailLinkPreview} */
|
|
const MailLinkPreview = this.env["mail.link.preview"];
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
|
|
const { message_id } = await parseRequestParams(request);
|
|
const [message] = MailMessage.search_read([["id", "=", message_id]]);
|
|
if (message.body.includes("https://make-link-preview.com")) {
|
|
const linkPreviewId = MailLinkPreview.create({
|
|
message_id: message.id,
|
|
og_description: "test description",
|
|
og_title: "Article title",
|
|
og_type: "article",
|
|
source_url: "https://make-link-preview.com",
|
|
});
|
|
BusBus._sendone(
|
|
MailMessage._bus_notification_target(message_id),
|
|
"mail.record/insert",
|
|
new mailDataHelpers.Store(MailLinkPreview.browse(linkPreviewId)).get_result()
|
|
);
|
|
}
|
|
}
|
|
|
|
registerRoute("/mail/link_preview/hide", mail_link_preview_hide);
|
|
/** @type {RouteCallback} */
|
|
async function mail_link_preview_hide(request) {
|
|
/** @type {import("mock_models").BusBus} */
|
|
const BusBus = this.env["bus.bus"];
|
|
/** @type {import("mock_models").MailLinkPreview} */
|
|
const MailLinkPreview = this.env["mail.link.preview"];
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
|
|
const { link_preview_ids } = await parseRequestParams(request);
|
|
for (const linkPreview of MailLinkPreview.browse(link_preview_ids)) {
|
|
BusBus._sendone(
|
|
MailMessage._bus_notification_target(linkPreview.message_id),
|
|
"mail.record/insert",
|
|
new mailDataHelpers.Store(MailMessage.browse(linkPreview.message_id), {
|
|
linkPreviews: mailDataHelpers.Store.many(
|
|
MailLinkPreview.browse(linkPreview.id),
|
|
"DELETE",
|
|
makeKwArgs({ only_id: true })
|
|
),
|
|
}).get_result()
|
|
);
|
|
}
|
|
return { link_preview_ids };
|
|
}
|
|
|
|
registerRoute("/mail/message/post", mail_message_post);
|
|
/** @type {RouteCallback} */
|
|
export async function mail_message_post(request) {
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
/** @type {import("mock_models").MailThread} */
|
|
const MailThread = this.env["mail.thread"];
|
|
/** @type {import("mock_models").ResPartner} */
|
|
const ResPartner = this.env["res.partner"];
|
|
|
|
const {
|
|
context,
|
|
post_data,
|
|
thread_id,
|
|
thread_model,
|
|
partner_emails,
|
|
partner_additional_values,
|
|
canned_response_ids,
|
|
} = await parseRequestParams(request);
|
|
if (canned_response_ids) {
|
|
for (const cannedResponseId of canned_response_ids) {
|
|
this.env["mail.canned.response"].write([cannedResponseId], {
|
|
last_used: serializeDateTime(DateTime.now()),
|
|
});
|
|
}
|
|
}
|
|
if (partner_emails) {
|
|
post_data.partner_ids = post_data.partner_ids || [];
|
|
for (const email of partner_emails) {
|
|
const partner = ResPartner._filter([["email", "=", email]]);
|
|
if (partner.length !== 0) {
|
|
post_data.partner_ids.push(partner[0].id);
|
|
} else {
|
|
const partner_id = ResPartner.create(
|
|
Object.assign({ email }, partner_additional_values[email] || {})
|
|
);
|
|
post_data.partner_ids.push(partner_id);
|
|
}
|
|
}
|
|
}
|
|
const finalData = {};
|
|
const allowedParams = [
|
|
"attachment_ids",
|
|
"body",
|
|
"message_type",
|
|
"partner_ids",
|
|
"subtype_xmlid",
|
|
];
|
|
if (thread_model === "discuss.channel") {
|
|
allowedParams.push("parent_id", "special_mentions");
|
|
}
|
|
for (const allowedParam of allowedParams) {
|
|
if (post_data[allowedParam] !== undefined) {
|
|
finalData[allowedParam] = post_data[allowedParam];
|
|
}
|
|
}
|
|
const kwargs = makeKwArgs({ ...finalData, context });
|
|
let messageId;
|
|
if (thread_model === "discuss.channel") {
|
|
messageId = DiscussChannel.message_post(thread_id, kwargs);
|
|
} else {
|
|
const model = this.env[thread_model];
|
|
messageId = MailThread.message_post.call(model, [thread_id], {
|
|
...kwargs,
|
|
model: thread_model,
|
|
});
|
|
}
|
|
return new mailDataHelpers.Store(
|
|
MailMessage.browse(messageId),
|
|
makeKwArgs({ for_current_user: true })
|
|
).get_result();
|
|
}
|
|
|
|
registerRoute("/mail/message/reaction", mail_message_reaction);
|
|
/** @type {RouteCallback} */
|
|
async function mail_message_reaction(request) {
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
const { action, content, message_id } = await parseRequestParams(request);
|
|
const partner_id = this.env.user?.partner_id ?? false;
|
|
const guest_id = this.env.cookie.get("dgid") ?? false;
|
|
const store = new mailDataHelpers.Store();
|
|
MailMessage._message_reaction(message_id, content, partner_id, guest_id, action, store);
|
|
return store.get_result();
|
|
}
|
|
|
|
registerRoute("/mail/message/translate", translate);
|
|
/** @type {RouteCallback} */
|
|
async function translate(request) {}
|
|
|
|
registerRoute("/mail/message/update_content", mail_message_update_content);
|
|
/** @type {RouteCallback} */
|
|
async function mail_message_update_content(request) {
|
|
/** @type {import("mock_models").BusBus} */
|
|
const BusBus = this.env["bus.bus"];
|
|
/** @type {import("mock_models").IrAttachment} */
|
|
const IrAttachment = this.env["ir.attachment"];
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
|
|
const { attachment_ids, body, message_id } = await parseRequestParams(request);
|
|
const [message] = MailMessage.browse(message_id);
|
|
const msg_values = {};
|
|
if (body !== null) {
|
|
const edit_label = "<span class='o-mail-Message-edited'/>";
|
|
msg_values.body = body === "" && attachment_ids.length === 0 ? "" : body + edit_label;
|
|
}
|
|
if (attachment_ids.length === 0) {
|
|
IrAttachment.unlink(message.attachment_ids);
|
|
} else {
|
|
const attachments = IrAttachment.browse(attachment_ids).filter(
|
|
(attachment) =>
|
|
attachment.res_model === "mail.compose.message" &&
|
|
attachment.create_uid === this.env.user?.id
|
|
);
|
|
IrAttachment.write(
|
|
attachments.map((attachment) => attachment.id),
|
|
{
|
|
model: message.model,
|
|
res_id: message.res_id,
|
|
}
|
|
);
|
|
msg_values.attachment_ids = attachment_ids;
|
|
}
|
|
MailMessage.write([message_id], msg_values);
|
|
if (body === "" && attachment_ids.length === 0) {
|
|
MailMessage.write([message_id], { pinned_at: false });
|
|
MailMessage._cleanup_side_records([message_id]);
|
|
}
|
|
BusBus._sendone(
|
|
MailMessage._bus_notification_target(message.id),
|
|
"mail.record/insert",
|
|
new mailDataHelpers.Store(MailMessage.browse(message.id), {
|
|
attachment_ids: mailDataHelpers.Store.many(IrAttachment.browse(message.attachment_ids)),
|
|
body: message.body,
|
|
pinned_at: message.pinned_at,
|
|
recipients: mailDataHelpers.Store.many(
|
|
this.env["res.partner"].browse(message.partner_ids),
|
|
makeKwArgs({ fields: ["name", "write_date"] })
|
|
),
|
|
}).get_result()
|
|
);
|
|
return new mailDataHelpers.Store(
|
|
MailMessage.browse(message_id),
|
|
makeKwArgs({ for_current_user: true })
|
|
).get_result();
|
|
}
|
|
|
|
registerRoute("/discuss/channel/<int:cid>/partner/<int:pid>/avatar_128", partnerAvatar128);
|
|
/** @type {RouteCallback} */
|
|
async function partnerAvatar128(request, { cid, pid }) {
|
|
return [cid, pid];
|
|
}
|
|
|
|
registerRoute("/mail/partner/from_email", mail_thread_partner_from_email);
|
|
/** @type {RouteCallback} */
|
|
async function mail_thread_partner_from_email(request) {
|
|
/** @type {import("mock_models").ResPartner} */
|
|
const ResPartner = this.env["res.partner"];
|
|
|
|
const { emails, additional_values = {} } = await parseRequestParams(request);
|
|
const partners = emails.map((email) => ResPartner.search([["email", "=", email]])[0]);
|
|
for (const index in partners) {
|
|
if (!partners[index]) {
|
|
const email = emails[index];
|
|
partners[index] = ResPartner.create({
|
|
email,
|
|
name: email,
|
|
...(additional_values[email] || {}),
|
|
});
|
|
}
|
|
}
|
|
return partners.map((partner_id) => {
|
|
const [partner] = ResPartner.browse(partner_id);
|
|
return { id: partner_id, name: partner.name, email: partner.email };
|
|
});
|
|
}
|
|
|
|
registerRoute("/mail/read_subscription_data", read_subscription_data);
|
|
/** @type {RouteCallback} */
|
|
async function read_subscription_data(request) {
|
|
/** @type {import("mock_models").MailFollowers} */
|
|
const MailFollowers = this.env["mail.followers"];
|
|
/** @type {import("mock_models").MailMessageSubtype} */
|
|
const MailMessageSubtype = this.env["mail.message.subtype"];
|
|
|
|
const { follower_id } = await parseRequestParams(request);
|
|
const [follower] = MailFollowers.browse(follower_id);
|
|
const subtypes = MailMessageSubtype._filter([
|
|
"&",
|
|
["hidden", "=", false],
|
|
"|",
|
|
["res_model", "=", follower.res_model],
|
|
["res_model", "=", false],
|
|
]);
|
|
const subtypes_list = subtypes.map((subtype) => {
|
|
const [parent] = MailMessageSubtype.browse(subtype.parent_id);
|
|
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;
|
|
}
|
|
|
|
registerRoute("/mail/rtc/session/update_and_broadcast", session_update_and_broadcast);
|
|
/** @type {RouteCallback} */
|
|
async function session_update_and_broadcast(request) {
|
|
/** @type {import("mock_models").DiscussChannelMember} */
|
|
const DiscussChannelMember = this.env["discuss.channel.member"];
|
|
/** @type {import("mock_models").DiscussChannelRtcSession} */
|
|
const DiscussChannelRtcSession = this.env["discuss.channel.rtc.session"];
|
|
|
|
const { session_id, values } = await parseRequestParams(request);
|
|
const [session] = DiscussChannelRtcSession.search_read([["id", "=", session_id]]);
|
|
const [currentChannelMember] = DiscussChannelMember.search_read([
|
|
["id", "=", session.channel_member_id[0]],
|
|
]);
|
|
if (session && currentChannelMember.partner_id[0] === serverState.partnerId) {
|
|
DiscussChannelRtcSession._update_and_broadcast(session.id, values);
|
|
}
|
|
}
|
|
|
|
registerRoute("/mail/starred/messages", discuss_starred_messages);
|
|
/** @type {RouteCallback} */
|
|
async function discuss_starred_messages(request) {
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
|
|
const { after, before, limit = 30, search_term } = await parseRequestParams(request);
|
|
const domain = [["starred_partner_ids", "in", [this.env.user.partner_id]]];
|
|
const res = MailMessage._message_fetch(domain, search_term, before, after, false, limit);
|
|
const { messages } = res;
|
|
delete res.messages;
|
|
return {
|
|
...res,
|
|
data: new mailDataHelpers.Store(
|
|
MailMessage.browse(messages.map((message) => message.id)),
|
|
makeKwArgs({ for_current_user: true })
|
|
).get_result(),
|
|
messages: mailDataHelpers.Store.many_ids(messages),
|
|
};
|
|
}
|
|
|
|
registerRoute("/mail/thread/data", mail_thread_data);
|
|
/** @type {RouteCallback} */
|
|
export async function mail_thread_data(request) {
|
|
const { request_list, thread_model, thread_id } = await parseRequestParams(request);
|
|
const [thread] = this.env[thread_model].browse(thread_id);
|
|
if (!thread) {
|
|
return new mailDataHelpers.Store("mail.thread", {
|
|
hasReadAccess: false,
|
|
hasWriteAccess: false,
|
|
id: thread_id,
|
|
model: thread_model,
|
|
}).get_result();
|
|
}
|
|
return new mailDataHelpers.Store(
|
|
this.env[thread_model].browse(thread_id),
|
|
makeKwArgs({ as_thread: true, request_list })
|
|
).get_result();
|
|
}
|
|
|
|
registerRoute("/mail/thread/messages", mail_thread_messages);
|
|
/** @type {RouteCallback} */
|
|
async function mail_thread_messages(request) {
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
|
|
const { after, around, before, limit, search_term, thread_id, thread_model } =
|
|
await parseRequestParams(request);
|
|
const domain = [
|
|
["res_id", "=", thread_id],
|
|
["model", "=", thread_model],
|
|
["message_type", "!=", "user_notification"],
|
|
];
|
|
const res = MailMessage._message_fetch(domain, search_term, before, after, around, limit);
|
|
const { messages } = res;
|
|
delete res.messages;
|
|
MailMessage.set_message_done(messages.map((message) => message.id));
|
|
return {
|
|
...res,
|
|
data: new mailDataHelpers.Store(
|
|
MailMessage.browse(messages.map((message) => message.id)),
|
|
makeKwArgs({ for_current_user: true })
|
|
).get_result(),
|
|
messages: mailDataHelpers.Store.many_ids(messages),
|
|
};
|
|
}
|
|
|
|
registerRoute("/mail/action", mail_action);
|
|
/** @type {RouteCallback} */
|
|
async function mail_action(request) {
|
|
return (await mailDataHelpers.processRequest.call(this, request)).get_result();
|
|
}
|
|
|
|
registerRoute("/mail/data", mail_data);
|
|
/** @type {RouteCallback} */
|
|
async function mail_data(request) {
|
|
return (await mailDataHelpers.processRequest.call(this, request)).get_result();
|
|
}
|
|
|
|
/** @type {RouteCallback} */
|
|
async function processRequest(request) {
|
|
/** @type {import("mock_models").DiscussChannel} */
|
|
const DiscussChannel = this.env["discuss.channel"];
|
|
/** @type {import("mock_models").DiscussChannelMember} */
|
|
const DiscussChannelMember = this.env["discuss.channel.member"];
|
|
/** @type {import("mock_models").MailGuest} */
|
|
const MailGuest = this.env["mail.guest"];
|
|
/** @type {import("mock_models").MailMessage} */
|
|
const MailMessage = this.env["mail.message"];
|
|
/** @type {import("mock_models").MailNotification} */
|
|
const MailNotification = this.env["mail.notification"];
|
|
/** @type {import("mock_models").ResPartner} */
|
|
const ResPartner = this.env["res.partner"];
|
|
/** @type {import("mock_models").ResUsers} */
|
|
const ResUsers = this.env["res.users"];
|
|
|
|
const store = new mailDataHelpers.Store();
|
|
const args = await parseRequestParams(request);
|
|
if ("init_messaging" in args) {
|
|
if (!MailGuest._get_guest_from_context() || !ResUsers._is_public(this.env.uid)) {
|
|
ResUsers._init_messaging([this.env.uid], store, args.context);
|
|
}
|
|
const guest = ResUsers._is_public(this.env.uid) && MailGuest._get_guest_from_context();
|
|
const members = DiscussChannelMember._filter([
|
|
guest ? ["guest_id", "=", guest.id] : ["partner_id", "=", this.env.user.partner_id],
|
|
"|",
|
|
["fold_state", "in", ["open", "folded"]],
|
|
["rtc_inviting_session_id", "!=", false],
|
|
]);
|
|
const channelsDomain = [["id", "in", members.map((m) => m.channel_id)]];
|
|
const { channelTypes } = args.init_messaging;
|
|
if (channelTypes) {
|
|
channelsDomain.push(["channel_type", "in", channelTypes]);
|
|
}
|
|
store.add(DiscussChannel.search(channelsDomain));
|
|
}
|
|
if (args.failures && this.env.user?.partner_id) {
|
|
const [partner] = ResPartner.browse(this.env.user.partner_id);
|
|
const messages = MailMessage._filter([
|
|
["author_id", "=", partner.id],
|
|
["res_id", "!=", 0],
|
|
["model", "!=", false],
|
|
["message_type", "!=", "user_notification"],
|
|
]).filter((message) => {
|
|
// Purpose is to simulate the following domain on mail.message:
|
|
// ['notification_ids.notification_status', 'in', ['bounce', 'exception']],
|
|
// But it's not supported by getRecords domain to follow a relation.
|
|
const notifications = MailNotification._filter([
|
|
["mail_message_id", "=", message.id],
|
|
["notification_status", "in", ["bounce", "exception"]],
|
|
]);
|
|
return notifications.length > 0;
|
|
});
|
|
messages.length = Math.min(messages.length, 100);
|
|
MailMessage._message_notifications_to_store(
|
|
messages.map((message) => message.id),
|
|
store
|
|
);
|
|
}
|
|
if (args.systray_get_activities && this.env.user?.partner_id) {
|
|
const bus_last_id = this.env["bus.bus"].lastBusNotificationId;
|
|
const groups = ResUsers._get_activity_groups();
|
|
store.add({
|
|
activityCounter: groups.reduce(
|
|
(counter, group) => counter + (group.total_count || 0),
|
|
0
|
|
),
|
|
activity_counter_bus_id: bus_last_id,
|
|
activityGroups: groups,
|
|
});
|
|
}
|
|
if (args.channels_as_member) {
|
|
const channels = DiscussChannel._get_channels_as_member();
|
|
store.add(
|
|
MailMessage.browse(
|
|
channels
|
|
.map(
|
|
(channel) =>
|
|
MailMessage._filter([
|
|
["model", "=", "discuss.channel"],
|
|
["res_id", "=", channel.id],
|
|
]).sort((a, b) => b.id - a.id)[0]
|
|
)
|
|
.filter((lastMessage) => lastMessage)
|
|
.map((message) => message.id)
|
|
),
|
|
makeKwArgs({ for_current_user: true })
|
|
);
|
|
store.add(channels.map((channel) => channel.id));
|
|
}
|
|
if (args.canned_responses) {
|
|
const domain = [
|
|
"|",
|
|
["create_uid", "=", this.env.user.id],
|
|
["group_ids", "in", this.env.user.groups_id],
|
|
];
|
|
store.add(this.env["mail.canned.response"].search(domain));
|
|
}
|
|
return store;
|
|
}
|
|
|
|
const ids_by_model = {
|
|
"mail.thread": ["model", "id"],
|
|
MessageReactions: ["message", "content"],
|
|
Rtc: [],
|
|
Store: [],
|
|
};
|
|
|
|
const MANY = Symbol("MANY");
|
|
const ONE = Symbol("ONE");
|
|
|
|
class Store {
|
|
constructor(data, values, as_thread, _delete, kwargs) {
|
|
this.data = new Map();
|
|
if (data) {
|
|
this.add(...arguments);
|
|
}
|
|
}
|
|
|
|
add(data, values, as_thread, _delete, kwargs) {
|
|
if (!data) {
|
|
return this;
|
|
}
|
|
kwargs = unmakeKwArgs(getKwArgs(arguments, "data", "values", "as_thread", "delete"));
|
|
data = kwargs.data;
|
|
delete kwargs.data;
|
|
values = kwargs.values;
|
|
delete kwargs.values;
|
|
as_thread = kwargs.as_thread;
|
|
delete kwargs.as_thread;
|
|
_delete = kwargs.delete ?? false;
|
|
delete kwargs.delete;
|
|
let model_name;
|
|
if (data instanceof models.Model) {
|
|
if (values) {
|
|
if (data.length !== 1) {
|
|
throw new Error(`expected single recordset ${data} with values`);
|
|
}
|
|
if (Object.keys(kwargs).length) {
|
|
throw new Error(
|
|
`expected empty kwargs with recordset ${data} values: ${kwargs}`
|
|
);
|
|
}
|
|
if (_delete) {
|
|
throw new Error(`deleted not expected for ${data} with values: ${values}`);
|
|
}
|
|
}
|
|
if (_delete) {
|
|
if (data.length !== 1) {
|
|
throw new Error(`expected single record ${data} with delete`);
|
|
}
|
|
if (values) {
|
|
throw new Error(`for ${data} expected empty values with delete: ${values}`);
|
|
}
|
|
}
|
|
const ids = data.map((idOrRecord) =>
|
|
typeof idOrRecord === "number" ? idOrRecord : idOrRecord.id
|
|
);
|
|
if (as_thread) {
|
|
if (_delete) {
|
|
this.add(
|
|
"mail.thread",
|
|
{ id: data[0].id, model: data._name },
|
|
makeKwArgs({ delete: _delete })
|
|
);
|
|
} else if (values) {
|
|
this.add("mail.thread", { id: data[0].id, model: data._name, ...values });
|
|
} else {
|
|
MockServer.env["mail.thread"]._thread_to_store.call(
|
|
MockServer.env[data._name],
|
|
ids,
|
|
this,
|
|
makeKwArgs(kwargs)
|
|
);
|
|
}
|
|
} else {
|
|
if (_delete) {
|
|
this.add(data._name, { id: ids[0] }, makeKwArgs({ delete: _delete }));
|
|
} else if (values) {
|
|
this.add(data._name, { id: ids[0], ...values });
|
|
} else {
|
|
MockServer.env[data._name]._to_store(ids, this, makeKwArgs(kwargs));
|
|
}
|
|
}
|
|
return this;
|
|
} else if (typeof data === "object") {
|
|
if (values) {
|
|
throw new Error(`expected empty values with dict ${data}: ${values}`);
|
|
}
|
|
if (Object.keys(kwargs).length) {
|
|
throw new Error(`expected empty kwargs with dict ${data}: ${kwargs}`);
|
|
}
|
|
if (as_thread) {
|
|
throw new Error(`expected not as_thread with dict ${data}: ${values}`);
|
|
}
|
|
model_name = "Store";
|
|
values = data;
|
|
} else {
|
|
if (Object.keys(kwargs).length) {
|
|
throw new Error(`expected empty kwargs with model name ${data}: ${kwargs}`);
|
|
}
|
|
if (as_thread) {
|
|
throw new Error(`expected not as_thread with model name ${data}: ${values}`);
|
|
}
|
|
model_name = data;
|
|
}
|
|
if (typeof model_name !== "string") {
|
|
throw new Error(`expected string for model name: ${model_name}: ${values}`);
|
|
}
|
|
const ids = ids_by_model[model_name] || ["id"];
|
|
// handle singleton model: update single record in place
|
|
if (!ids.length) {
|
|
if (typeof values !== "object") {
|
|
throw new Error(`expected dict for singleton ${model_name}: ${values}`);
|
|
}
|
|
if (_delete) {
|
|
throw new Error(`Singleton ${model_name} cannot be deleted`);
|
|
}
|
|
if (!this.data.has(model_name)) {
|
|
this.data.set(model_name, {});
|
|
}
|
|
this._add_values(values, model_name);
|
|
return this;
|
|
}
|
|
// handle model with ids: add or update existing records based on ids
|
|
if (!Array.isArray(values)) {
|
|
if (!values) {
|
|
return this;
|
|
}
|
|
values = [values];
|
|
}
|
|
if (!values.length) {
|
|
return this;
|
|
}
|
|
let records = this.data.get(model_name);
|
|
if (!records) {
|
|
records = new Map();
|
|
this.data.set(model_name, records);
|
|
}
|
|
for (const vals of values) {
|
|
if (typeof vals !== "object") {
|
|
throw new Error(`expected dict for ${model_name}: ${vals}`);
|
|
}
|
|
for (const i of ids) {
|
|
if (!vals[i]) {
|
|
throw new Error(`missing id ${i} in ${model_name}: ${vals}`);
|
|
}
|
|
}
|
|
const index = ids.map((i) => vals[i]).join(" AND ");
|
|
if (!records.has(index)) {
|
|
records.set(index, {});
|
|
}
|
|
this._add_values(vals, model_name, index);
|
|
if (_delete) {
|
|
records.get(index)._DELETE = true;
|
|
} else {
|
|
delete records.get(index)._DELETE;
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
_add_values(values, model_name, index) {
|
|
const target = index ? this.data.get(model_name).get(index) : this.data.get(model_name);
|
|
for (const [key, val] of Object.entries(values)) {
|
|
if (key === "_DELETE") {
|
|
throw new Error(`invalid key ${key} in ${model_name}: ${values}`);
|
|
}
|
|
if (Array.isArray(val) && val[0] === ONE) {
|
|
const [, subrecord, as_thread, only_id, subrecord_kwargs] = val;
|
|
if (subrecord && !(subrecord instanceof models.Model)) {
|
|
throw new Error(`expected recordset for one ${key}: ${subrecord}`);
|
|
}
|
|
if (subrecord && subrecord.length && !only_id) {
|
|
this.add(subrecord, makeKwArgs({ as_thread, ...subrecord_kwargs }));
|
|
}
|
|
target[key] = Store.one_id(subrecord, makeKwArgs({ as_thread }));
|
|
} else if (Array.isArray(val) && val[0] === MANY) {
|
|
const [, subrecords, mode, as_thread, only_id, subrecords_kwargs] = val;
|
|
if (subrecords && !(subrecords instanceof models.Model)) {
|
|
throw new Error(`expected recordset for many ${key}: ${subrecords}`);
|
|
}
|
|
if (!["ADD", "DELETE", "REPLACE"].includes(mode)) {
|
|
throw new Error(`invalid mode for many ${key}: ${mode} `);
|
|
}
|
|
if (subrecords && subrecords.length && !only_id) {
|
|
this.add(subrecords, makeKwArgs({ as_thread, ...subrecords_kwargs }));
|
|
}
|
|
const rel_val = Store.many_ids(subrecords, mode, makeKwArgs({ as_thread }));
|
|
target[key] =
|
|
key in target && mode !== "REPLACE" ? target[key].concat(rel_val) : rel_val;
|
|
} else {
|
|
target[key] = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
get_result() {
|
|
const res = {};
|
|
for (const [model_name, records] of this.data) {
|
|
const ids = ids_by_model[model_name] || ["id"];
|
|
if (!ids.length) {
|
|
// singleton
|
|
res[model_name] = { ...records };
|
|
} else {
|
|
res[model_name] = [...records.values()].map((record) => ({ ...record }));
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
toJSON() {
|
|
throw Error(
|
|
"Converting Store to JSON is not supported, you might want to call 'get_result()' instead."
|
|
);
|
|
}
|
|
|
|
static many(records, mode = "REPLACE", as_thread, only_id, kwargs) {
|
|
kwargs = getKwArgs(arguments, "records", "mode");
|
|
records = kwargs.records;
|
|
delete kwargs.records;
|
|
mode = kwargs.mode ?? "REPLACE";
|
|
delete kwargs.mode;
|
|
as_thread = kwargs.as_thread;
|
|
delete kwargs.as_thread;
|
|
only_id = kwargs.only_id;
|
|
delete kwargs.only_id;
|
|
if (records && !(records instanceof models.Model)) {
|
|
throw new Error(`expected recordset for many: ${records}`);
|
|
}
|
|
return [MANY, records, mode, as_thread, only_id, makeKwArgs(kwargs)];
|
|
}
|
|
|
|
static one(records, as_thread, only_id, kwargs) {
|
|
kwargs = getKwArgs(arguments, "records");
|
|
records = kwargs.records;
|
|
delete kwargs.records;
|
|
as_thread = kwargs.as_thread;
|
|
delete kwargs.as_thread;
|
|
only_id = kwargs.only_id;
|
|
delete kwargs.only_id;
|
|
if (records && !(records instanceof models.Model)) {
|
|
throw new Error(`expected recordset for one: ${records}`);
|
|
}
|
|
return [ONE, records, as_thread, only_id, makeKwArgs(kwargs)];
|
|
}
|
|
|
|
static many_ids(records, mode = "REPLACE", as_thread) {
|
|
const kwargs = getKwArgs(arguments, "records", "mode");
|
|
records = kwargs.records;
|
|
mode = kwargs.mode ?? "REPLACE";
|
|
as_thread = kwargs.as_thread;
|
|
if (records && !(records instanceof models.Model)) {
|
|
throw new Error(`expected recordset for many_ids: ${records}`);
|
|
}
|
|
if (!["ADD", "DELETE", "REPLACE"].includes(mode)) {
|
|
throw new Error(`invalid mode for many_ids: ${mode} `);
|
|
}
|
|
let res = records.map((record) =>
|
|
Store.one_id(records.browse(record.id), makeKwArgs({ as_thread }))
|
|
);
|
|
if (records._name === "mail.message.reaction") {
|
|
res = [];
|
|
const reactionGroups = groupBy(records, (r) => [r.message_id, r.content]);
|
|
for (const groupId in reactionGroups) {
|
|
const { message_id, content } = reactionGroups[groupId][0];
|
|
res.push({ message: message_id, content: content });
|
|
}
|
|
}
|
|
if (mode === "ADD") {
|
|
res = [["ADD", res]];
|
|
} else if (mode === "DELETE") {
|
|
res = [["DELETE", res]];
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static one_id(records, as_thread) {
|
|
const kwargs = getKwArgs(arguments, "records");
|
|
records = kwargs.records;
|
|
as_thread = kwargs.as_thread;
|
|
if (!records) {
|
|
return false;
|
|
}
|
|
if (!(records instanceof models.Model)) {
|
|
throw new Error(`expected recordset for one_id: ${records}`);
|
|
}
|
|
if (records.length > 1) {
|
|
throw new Error(`expected none or single record for one_id: ${records}`);
|
|
}
|
|
const [record] = records;
|
|
if (!record) {
|
|
return false;
|
|
}
|
|
if (as_thread) {
|
|
return { id: record.id, model: records._name };
|
|
}
|
|
if (records._name === "discuss.channel") {
|
|
return { id: record.id, model: "discuss.channel" };
|
|
}
|
|
if (records._name === "mail.guest") {
|
|
return { id: record.id, type: "guest" };
|
|
}
|
|
if (records._name === "res.partner") {
|
|
return { id: record.id, type: "partner" };
|
|
}
|
|
return record.id;
|
|
}
|
|
}
|
|
|
|
export const mailDataHelpers = {
|
|
processRequest,
|
|
Store,
|
|
};
|