edit messages
This commit is contained in:
@@ -234,6 +234,8 @@ var Chat = {
|
|||||||
|
|
||||||
if (text == '') {
|
if (text == '') {
|
||||||
Chat.disableSending();
|
Chat.disableSending();
|
||||||
|
} else if (Chat.getOmemoState() == 'yes') {
|
||||||
|
Chat.sendEncryptedCorrection(jid, textarea.dataset.mid, text, isMuc);
|
||||||
} else {
|
} else {
|
||||||
let correct = Chat_ajaxHttpDaemonCorrect(jid, textarea.dataset.mid, text);
|
let correct = Chat_ajaxHttpDaemonCorrect(jid, textarea.dataset.mid, text);
|
||||||
correct.then(e => {
|
correct.then(e => {
|
||||||
@@ -301,6 +303,35 @@ var Chat = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
sendEncryptedCorrection: function (jid, mid, text, isMuc) {
|
||||||
|
let omemo = ChatOmemo.encrypt(jid, text, isMuc);
|
||||||
|
|
||||||
|
if (!omemo) {
|
||||||
|
Chat.failedMessage(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
omemo.then(omemoheader => {
|
||||||
|
let id = omemoheader.id = crypto.randomUUID();
|
||||||
|
Chat.tempMessages[id] = text;
|
||||||
|
|
||||||
|
Chat_ajaxHttpDaemonCorrect(jid, mid, null, omemoheader)
|
||||||
|
.then(e => Chat.sentMessage())
|
||||||
|
.catch(e => Chat.failedMessage(text));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// The server cannot read encrypted messages, the text comes from this browser
|
||||||
|
editEncrypted: function (id, mid) {
|
||||||
|
if (typeof ChatOmemoDB == 'undefined') return;
|
||||||
|
|
||||||
|
ChatOmemoDB.getMessage(id).then(text => {
|
||||||
|
if (typeof text == 'string' && text != '' && !AESGCM_REGEX.test(text.trim())) {
|
||||||
|
Chat.setTextarea(text, mid);
|
||||||
|
} else {
|
||||||
|
Toast.send(Chat.action_impossible_encrypted_error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
sentId: function (id) {
|
sentId: function (id) {
|
||||||
if (Chat.tempMessages[id]) {
|
if (Chat.tempMessages[id]) {
|
||||||
ChatOmemoDB.putMessage(id, Chat.tempMessages[id]);
|
ChatOmemoDB.putMessage(id, Chat.tempMessages[id]);
|
||||||
@@ -405,11 +436,7 @@ var Chat = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Chat.isEncrypted()) {
|
|
||||||
Chat_ajaxLast(textarea.dataset.jid, Boolean(textarea.dataset.muc));
|
Chat_ajaxLast(textarea.dataset.jid, Boolean(textarea.dataset.muc));
|
||||||
} else {
|
|
||||||
Toast.send(Chat.action_impossible_encrypted_error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
resolveMessage: function (mid) {
|
resolveMessage: function (mid) {
|
||||||
ChatActions_ajaxResolveMessage(mid);
|
ChatActions_ajaxResolveMessage(mid);
|
||||||
@@ -443,7 +470,7 @@ var Chat = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key == 'ArrowUp' && !Chat.isEncrypted()) {
|
if (event.key == 'ArrowUp') {
|
||||||
Chat.editPrevious();
|
Chat.editPrevious();
|
||||||
} else if (event.key == 'Escape'
|
} else if (event.key == 'Escape'
|
||||||
&& (this.value == '' || Chat.edit == true)) {
|
&& (this.value == '' || Chat.edit == true)) {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ CREATE INDEX IF NOT EXISTS messages_published ON messages (published);
|
|||||||
CREATE INDEX IF NOT EXISTS messages_user_stanzaid ON messages (user_id, stanzaid);
|
CREATE INDEX IF NOT EXISTS messages_user_stanzaid ON messages (user_id, stanzaid);
|
||||||
CREATE INDEX IF NOT EXISTS messages_user_originid ON messages (user_id, originid);
|
CREATE INDEX IF NOT EXISTS messages_user_originid ON messages (user_id, originid);
|
||||||
CREATE INDEX IF NOT EXISTS messages_user_messageid ON messages (user_id, messageid);
|
CREATE INDEX IF NOT EXISTS messages_user_messageid ON messages (user_id, messageid);
|
||||||
|
CREATE INDEX IF NOT EXISTS messages_user_replaceid ON messages (user_id, replaceid);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS message_files (
|
CREATE TABLE IF NOT EXISTS message_files (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -1096,7 +1097,7 @@ pub async fn messages_search(
|
|||||||
|
|
||||||
let rows: Vec<(i64, String)> = sqlx::query_as(AssertSqlSafe(format!(
|
let rows: Vec<(i64, String)> = sqlx::query_as(AssertSqlSafe(format!(
|
||||||
"SELECT mid, body FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?) AND type IN ({types_sql})
|
"SELECT mid, body FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?) AND type IN ({types_sql})
|
||||||
AND encrypted = 0 AND retracted = 0 AND body IS NOT NULL ORDER BY published DESC"
|
AND encrypted = 0 AND retracted = 0 AND body IS NOT NULL AND {NOT_REPLACED} ORDER BY published DESC"
|
||||||
)))
|
)))
|
||||||
.bind(user)
|
.bind(user)
|
||||||
.bind(jid)
|
.bind(jid)
|
||||||
@@ -1131,13 +1132,20 @@ pub async fn messages_search(
|
|||||||
Ok(q.bind(limit).fetch_all(db).await?)
|
Ok(q.bind(limit).fetch_all(db).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Skips the messages that were corrected by their author (XEP-0308): only the last version is searched
|
||||||
|
/// (corrections all point to the first version, so a later one hides the earlier ones too)
|
||||||
|
const NOT_REPLACED: &str = "NOT EXISTS (SELECT 1 FROM messages r WHERE r.user_id = messages.user_id AND r.replaceid IS NOT NULL
|
||||||
|
AND (r.replaceid IN (messages.id, messages.messageid, messages.originid)
|
||||||
|
OR r.replaceid = messages.replaceid AND r.mid > messages.mid)
|
||||||
|
AND r.jidfrom = messages.jidfrom AND (r.type = 'chat' OR r.resource IS messages.resource))";
|
||||||
|
|
||||||
/// OMEMO messages of a conversation, newest first: (mid, id, messageid, published).
|
/// OMEMO messages of a conversation, newest first: (mid, id, messageid, published).
|
||||||
/// The browser looks their decrypted bodies up with id or messageid.
|
/// The browser looks their decrypted bodies up with id or messageid.
|
||||||
pub async fn encrypted_index(db: &Db, user: &str, jid: &str, muc: bool) -> Result<Vec<(i64, String, Option<String>, String)>> {
|
pub async fn encrypted_index(db: &Db, user: &str, jid: &str, muc: bool) -> Result<Vec<(i64, String, Option<String>, String)>> {
|
||||||
let types_sql = conversation_types(muc);
|
let types_sql = conversation_types(muc);
|
||||||
Ok(sqlx::query_as(AssertSqlSafe(format!(
|
Ok(sqlx::query_as(AssertSqlSafe(format!(
|
||||||
"SELECT mid, id, messageid, published FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?)
|
"SELECT mid, id, messageid, published FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?)
|
||||||
AND type IN ({types_sql}) AND encrypted = 1 AND retracted = 0 ORDER BY published DESC LIMIT 5000"
|
AND type IN ({types_sql}) AND encrypted = 1 AND retracted = 0 AND {NOT_REPLACED} ORDER BY published DESC LIMIT 5000"
|
||||||
)))
|
)))
|
||||||
.bind(user)
|
.bind(user)
|
||||||
.bind(jid)
|
.bind(jid)
|
||||||
@@ -1300,6 +1308,23 @@ mod tests {
|
|||||||
let index = encrypted_index(&db, me, bob, false).await.unwrap();
|
let index = encrypted_index(&db, me, bob, false).await.unwrap();
|
||||||
assert_eq!(index.iter().map(|e| e.0).collect::<Vec<_>>(), vec![secret_mid]);
|
assert_eq!(index.iter().map(|e| e.0).collect::<Vec<_>>(), vec![secret_mid]);
|
||||||
|
|
||||||
|
// Corrections: only the last version is searched
|
||||||
|
let typo = Message { id: "e1".into(), messageid: Some("e1".into()), body: Some("hẹn gặp lúc 9h".into()), published: days_ago(1), ..m.clone() };
|
||||||
|
let fixed = Message { id: "e2".into(), replaceid: Some("e1".into()), body: Some("hẹn gặp lúc 10h".into()), published: days_ago(1), ..m.clone() };
|
||||||
|
message_save(&db, &typo).await.unwrap();
|
||||||
|
let fixed_mid = message_save(&db, &fixed).await.unwrap();
|
||||||
|
assert!(messages_search(&db, me, bob, false, "9h", &[], 20).await.unwrap().is_empty());
|
||||||
|
let found = messages_search(&db, me, bob, false, "hen gap", &[], 20).await.unwrap();
|
||||||
|
assert_eq!(found.iter().map(|m| m.mid).collect::<Vec<_>>(), vec![fixed_mid]);
|
||||||
|
let again = Message { id: "e4".into(), replaceid: Some("e1".into()), body: Some("hẹn gặp lúc 11h".into()), published: days_ago(1), ..m.clone() };
|
||||||
|
let again_mid = message_save(&db, &again).await.unwrap();
|
||||||
|
let found = messages_search(&db, me, bob, false, "hen gap", &[], 20).await.unwrap();
|
||||||
|
assert_eq!(found.iter().map(|m| m.mid).collect::<Vec<_>>(), vec![again_mid]);
|
||||||
|
// Someone else cannot hide a message with a fake correction
|
||||||
|
let fake = Message { id: "e3".into(), jidfrom: "eve@example.org".into(), replaceid: Some("s1".into()), published: days_ago(1), ..m.clone() };
|
||||||
|
message_save(&db, &fake).await.unwrap();
|
||||||
|
assert_eq!(messages_search(&db, me, bob, false, "xin chao", &[], 20).await.unwrap().len(), 1);
|
||||||
|
|
||||||
// Expiry: an old message goes, a recent one stays
|
// Expiry: an old message goes, a recent one stays
|
||||||
let old = Message { id: "old".into(), published: "2020-01-01 10:00:00".into(), ..m.clone() };
|
let old = Message { id: "old".into(), published: "2020-01-01 10:00:00".into(), ..m.clone() };
|
||||||
let new = Message { id: "new".into(), published: crate::utils::now_sql(), ..m.clone() };
|
let new = Message { id: "new".into(), published: crate::utils::now_sql(), ..m.clone() };
|
||||||
|
|||||||
+58
-31
@@ -40,13 +40,13 @@ pub async fn call(ctx: &Ctx, func: &str, p: &P<'_>) -> bool {
|
|||||||
"ajaxGetRoom" => get_room(ctx, &p.str(0), p.b(1)).await,
|
"ajaxGetRoom" => get_room(ctx, &p.str(0), p.b(1)).await,
|
||||||
"ajaxHttpDaemonSendMessage" => send_from_params(ctx, p).await,
|
"ajaxHttpDaemonSendMessage" => send_from_params(ctx, p).await,
|
||||||
"ajaxHttpDaemonCorrect" => {
|
"ajaxHttpDaemonCorrect" => {
|
||||||
// chat.js calls it with (to, mid, message)
|
// chat.js calls it with (to, mid, message, omemo)
|
||||||
let (to, mid, message) = if p.i(1).is_some() && p.v(3).is_none() {
|
let (to, mid, message, omemo) = if p.i(1).is_some() {
|
||||||
(p.str(0), p.i(1).unwrap_or(0), p.str(2))
|
(p.str(0), p.i(1).unwrap_or(0), p.str(2), p.v(3).cloned())
|
||||||
} else {
|
} else {
|
||||||
(p.str(1), p.i(2).unwrap_or(0), p.str(3))
|
(p.str(1), p.i(2).unwrap_or(0), p.str(3), p.v(4).cloned())
|
||||||
};
|
};
|
||||||
correct(ctx, &to, mid, &message).await;
|
correct(ctx, &to, mid, &message, omemo).await;
|
||||||
}
|
}
|
||||||
"ajaxHttpDaemonSendReaction" => send_reaction(ctx, p.i(0).unwrap_or(0), &p.str(1)).await,
|
"ajaxHttpDaemonSendReaction" => send_reaction(ctx, p.i(0).unwrap_or(0), &p.str(1)).await,
|
||||||
"ajaxRefreshMessage" => refresh_message(ctx, p.i(0).unwrap_or(0)).await,
|
"ajaxRefreshMessage" => refresh_message(ctx, p.i(0).unwrap_or(0)).await,
|
||||||
@@ -1080,12 +1080,59 @@ fn omemo_element(o: &Value) -> minidom::Element {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn correct(ctx: &Ctx, to: &str, mid: i64, message: &str) {
|
async fn correct(ctx: &Ctx, to: &str, mid: i64, message: &str, omemo: Option<Value>) {
|
||||||
let me = ctx.jid();
|
let me = ctx.jid();
|
||||||
let Some(original) = db::message_by_mid(ctx.db(), &me, mid).await.ok().flatten() else { return };
|
let Some(original) = db::message_by_mid(ctx.db(), &me, mid).await.ok().flatten() else { return };
|
||||||
|
if !is_mine(ctx, &original) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let muc = original.is_muc();
|
let muc = original.is_muc();
|
||||||
let to = if to.is_empty() || to == "false" { original.jid().to_string() } else { to.to_string() };
|
let to = if to.is_empty() || to == "false" { original.jid().to_string() } else { to.to_string() };
|
||||||
send_message(ctx, &utils::generate_uuid(), &to, Some(message.to_string()), muc, None, None, Some(original), None).await;
|
// Encrypted corrections keep the id chosen by the browser, it stores the plaintext under it
|
||||||
|
let id = omemo
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|o| o.get("id"))
|
||||||
|
.and_then(|i| i.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.unwrap_or_else(utils::generate_uuid);
|
||||||
|
send_message(ctx, &id, &to, Some(message.to_string()), muc, None, None, Some(original), omemo).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_mine(ctx: &Ctx, m: &Message) -> bool {
|
||||||
|
if m.is_muc() {
|
||||||
|
m.resource.is_some() && rooms::my_nick(ctx, &m.jidfrom) == m.resource
|
||||||
|
} else {
|
||||||
|
m.jidfrom == ctx.jid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Corrections always point to the first version of a message (XEP-0308)
|
||||||
|
async fn original_mid(ctx: &Ctx, m: &Message) -> i64 {
|
||||||
|
if let Some(rid) = &m.replaceid {
|
||||||
|
if let Ok(Some(orig)) = db::message_resolve_reference(ctx.db(), &ctx.jid(), m.jid(), rid).await {
|
||||||
|
return orig.mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Put the latest version of one of my messages in the textarea to correct it
|
||||||
|
async fn start_edit(ctx: &Ctx, m: Message) {
|
||||||
|
if !is_mine(ctx, &m) || m.retracted || m.type_ != "chat" && m.type_ != "groupchat" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if db::message_file(ctx.db(), m.mid).await.ok().flatten().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mid = original_mid(ctx, &m).await;
|
||||||
|
if m.encrypted {
|
||||||
|
// Only the browser can read it
|
||||||
|
if let Some(id) = m.messageid.clone().or(m.originid.clone()) {
|
||||||
|
rpc!(ctx, "Chat.editEncrypted", id, mid);
|
||||||
|
}
|
||||||
|
} else if let Some(body) = m.body.filter(|b| !b.is_empty()) {
|
||||||
|
rpc!(ctx, "Chat.setTextarea", body, mid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_reaction(ctx: &Ctx, mid: i64, emoji: &str) {
|
async fn send_reaction(ctx: &Ctx, mid: i64, emoji: &str) {
|
||||||
@@ -1135,36 +1182,16 @@ async fn send_reaction(ctx: &Ctx, mid: i64, emoji: &str) {
|
|||||||
async fn last(ctx: &Ctx, to: &str, muc: bool) {
|
async fn last(ctx: &Ctx, to: &str, muc: bool) {
|
||||||
let me = ctx.jid();
|
let me = ctx.jid();
|
||||||
let messages = db::messages_for(ctx.db(), &me, to, muc, None, None, 20).await.unwrap_or_default();
|
let messages = db::messages_for(ctx.db(), &me, to, muc, None, None, 20).await.unwrap_or_default();
|
||||||
let my_nick = if muc { rooms::my_nick(ctx, to) } else { None };
|
if let Some(m) = messages.into_iter().find(|m| is_mine(ctx, m)) {
|
||||||
let mine = messages.into_iter().find(|m| {
|
start_edit(ctx, m).await;
|
||||||
if muc { m.resource.is_some() && m.resource == my_nick } else { m.jidfrom == me }
|
|
||||||
});
|
|
||||||
let Some(m) = mine else { return };
|
|
||||||
if m.retracted || m.encrypted || m.type_ != "chat" && m.type_ != "groupchat" {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if db::message_file(ctx.db(), m.mid).await.ok().flatten().is_some() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Use the original message id so the correction replaces it
|
|
||||||
let mut mid = m.mid;
|
|
||||||
if let Some(rid) = &m.replaceid {
|
|
||||||
if let Ok(Some(orig)) = db::message_resolve_reference(ctx.db(), &me, to, rid).await {
|
|
||||||
mid = orig.mid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(body) = m.body.filter(|b| !b.is_empty()) {
|
|
||||||
rpc!(ctx, "Chat.setTextarea", body, mid);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn edit(ctx: &Ctx, mid: i64) {
|
async fn edit(ctx: &Ctx, mid: i64) {
|
||||||
let me = ctx.jid();
|
let me = ctx.jid();
|
||||||
let Some(m) = db::message_by_mid(ctx.db(), &me, mid).await.ok().flatten() else { return };
|
if let Some(m) = db::message_by_mid(ctx.db(), &me, mid).await.ok().flatten() {
|
||||||
if db::message_file(ctx.db(), mid).await.ok().flatten().is_some() {
|
start_edit(ctx, m).await;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
rpc!(ctx, "Chat.setTextarea", m.body.unwrap_or_default(), mid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reply(ctx: &Ctx, mid: i64) {
|
async fn reply(ctx: &Ctx, mid: i64) {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ async fn show_message_dialog(ctx: &Ctx, mid: i64) {
|
|||||||
|
|
||||||
let mine = if m.is_muc() { rooms::my_nick(ctx, &m.jidfrom) == m.resource && m.resource.is_some() } else { m.jidfrom == me };
|
let mine = if m.is_muc() { rooms::my_nick(ctx, &m.jidfrom) == m.resource && m.resource.is_some() } else { m.jidfrom == me };
|
||||||
let has_file = db::message_file(ctx.db(), mid).await.ok().flatten().is_some();
|
let has_file = db::message_file(ctx.db(), mid).await.ok().flatten().is_some();
|
||||||
let editable = mine && !m.encrypted && !has_file;
|
let editable = mine && !has_file && !m.retracted;
|
||||||
let moderator = m.is_muc()
|
let moderator = m.is_muc()
|
||||||
&& m.stanzaid.is_some()
|
&& m.stanzaid.is_some()
|
||||||
&& ctx
|
&& ctx
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ pub static WIDGETS: &[WidgetDef] = &[
|
|||||||
("ajaxGetRoom", &["room", "noConnect"]),
|
("ajaxGetRoom", &["room", "noConnect"]),
|
||||||
("ajaxGetSpaceRoom", &["server", "node", "roomId"]),
|
("ajaxGetSpaceRoom", &["server", "node", "roomId"]),
|
||||||
("ajaxHttpDaemonSendMessage", &["id", "to", "message", "muc", "file", "replyToMid", "mucReceipts", "omemo"]),
|
("ajaxHttpDaemonSendMessage", &["id", "to", "message", "muc", "file", "replyToMid", "mucReceipts", "omemo"]),
|
||||||
("ajaxHttpDaemonCorrect", &["id", "to", "mid", "message"]),
|
("ajaxHttpDaemonCorrect", &["id", "to", "mid", "message", "omemo"]),
|
||||||
("ajaxHttpDaemonSendReaction", &["mid", "emoji"]),
|
("ajaxHttpDaemonSendReaction", &["mid", "emoji"]),
|
||||||
("ajaxRefreshMessage", &["mid"]),
|
("ajaxRefreshMessage", &["mid"]),
|
||||||
("ajaxLast", &["to", "muc"]),
|
("ajaxLast", &["to", "muc"]),
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""Message corrections (XEP-0308), in clear and in OMEMO chats."""
|
||||||
|
import asyncio
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
from browser import login, BASE, OUT
|
||||||
|
|
||||||
|
|
||||||
|
async def send(page, text):
|
||||||
|
await page.fill("#chat_textarea", text)
|
||||||
|
await page.press("#chat_textarea", "Enter")
|
||||||
|
await page.wait_for_timeout(2500)
|
||||||
|
|
||||||
|
|
||||||
|
async def edit_last(page, text):
|
||||||
|
"""↑ in the empty textarea, then replace the text"""
|
||||||
|
await page.fill("#chat_textarea", "")
|
||||||
|
await page.press("#chat_textarea", "ArrowUp")
|
||||||
|
await page.wait_for_timeout(1500)
|
||||||
|
print(" textarea after ↑:", repr(await page.input_value("#chat_textarea")))
|
||||||
|
await send(page, text)
|
||||||
|
|
||||||
|
|
||||||
|
async def bubbles(page, n=4):
|
||||||
|
return (await page.locator("#chat_widget .bubble p").all_inner_texts())[-n:]
|
||||||
|
|
||||||
|
|
||||||
|
async def open_chat(page, jid):
|
||||||
|
await page.goto(BASE + "/chat/" + jid)
|
||||||
|
await page.wait_for_selector("#chat_textarea", timeout=15000)
|
||||||
|
await page.wait_for_timeout(5000)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch()
|
||||||
|
actx, alice, aerr = await login(browser, "alice@localhost", "alicepass", "editalice")
|
||||||
|
bctx, bob, berr = await login(browser, "bob@localhost", "bobpass", "editbob")
|
||||||
|
await alice.wait_for_timeout(4000)
|
||||||
|
await open_chat(alice, "bob@localhost")
|
||||||
|
await open_chat(bob, "alice@localhost")
|
||||||
|
|
||||||
|
# Clear text
|
||||||
|
await alice.evaluate("ChatOmemo.disableContactState('bob@localhost')")
|
||||||
|
await alice.wait_for_timeout(2000)
|
||||||
|
await send(alice, "Hẹn gặp lúc 9h")
|
||||||
|
await edit_last(alice, "Hẹn gặp lúc 10h")
|
||||||
|
print("clear, alice:", await bubbles(alice, 2))
|
||||||
|
print("clear, bob:", await bubbles(bob, 2))
|
||||||
|
print("edited marks on bob:", await bob.locator("#chat_widget .edited").count())
|
||||||
|
|
||||||
|
# OMEMO
|
||||||
|
await alice.evaluate("ChatOmemo.enableContactState('bob@localhost', false)")
|
||||||
|
await alice.wait_for_timeout(5000)
|
||||||
|
print("alice omemo:", await alice.evaluate("Chat.getOmemoState()"))
|
||||||
|
await send(alice, "Bí mật: mật khẩu wifi là 1234")
|
||||||
|
await edit_last(alice, "Bí mật: mật khẩu wifi là 5678")
|
||||||
|
await alice.screenshot(path=f"{OUT}/edit_1_alice.png")
|
||||||
|
print("omemo, alice:", await bubbles(alice, 3))
|
||||||
|
print("omemo, bob:", await bubbles(bob, 3))
|
||||||
|
|
||||||
|
# Second edit, from the message menu
|
||||||
|
mid = await alice.evaluate("[...document.querySelectorAll('#chat_widget .bubble [data-mid]')].pop().dataset.mid")
|
||||||
|
await alice.evaluate(f"Chat_ajaxEdit({mid})")
|
||||||
|
await alice.wait_for_timeout(1500)
|
||||||
|
print(" textarea from menu:", repr(await alice.input_value("#chat_textarea")))
|
||||||
|
await send(alice, "Bí mật: mật khẩu wifi là 9999")
|
||||||
|
await bob.wait_for_timeout(2000)
|
||||||
|
await bob.screenshot(path=f"{OUT}/edit_2_bob.png")
|
||||||
|
print("omemo 2nd edit, bob:", await bubbles(bob, 3))
|
||||||
|
|
||||||
|
# After a reload only the last versions are shown
|
||||||
|
await open_chat(bob, "alice@localhost")
|
||||||
|
await open_chat(alice, "bob@localhost")
|
||||||
|
print("reload, bob:", await bubbles(bob, 3))
|
||||||
|
print("reload, alice:", await bubbles(alice, 3))
|
||||||
|
await bob.screenshot(path=f"{OUT}/edit_3_bob_reload.png")
|
||||||
|
|
||||||
|
# Search finds the new text, not the old one
|
||||||
|
await bob.evaluate("ChatActions_ajaxShowSearchDialog('alice@localhost', false)")
|
||||||
|
await bob.wait_for_selector("form[name=search] input", timeout=10000)
|
||||||
|
await bob.wait_for_timeout(1000)
|
||||||
|
for q in ("wifi", "1234", "9h", "10h"):
|
||||||
|
await bob.fill("form[name=search] input", q)
|
||||||
|
await bob.wait_for_timeout(2500)
|
||||||
|
print(f"search {q!r}:", await bob.locator("#chat_search #message_preview li p").all_inner_texts())
|
||||||
|
|
||||||
|
for e in aerr + berr:
|
||||||
|
print(e)
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
Reference in New Issue
Block a user