fix: handle status edit events (#2325)

This commit is contained in:
Nolan Lawson 2022-12-18 11:20:17 -08:00 committed by GitHub
parent 3edfed971f
commit 02f1dad098
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 103 additions and 17 deletions

View File

@ -2,8 +2,9 @@ import { mark, stop } from '../../_utils/marks.js'
import { deleteStatus } from '../deleteStatuses.js' import { deleteStatus } from '../deleteStatuses.js'
import { addStatusOrNotification } from '../addStatusOrNotification.js' import { addStatusOrNotification } from '../addStatusOrNotification.js'
import { emit } from '../../_utils/eventBus.js' import { emit } from '../../_utils/eventBus.js'
import { updateStatus } from '../updateStatus.js'
const KNOWN_EVENTS = ['update', 'delete', 'notification', 'conversation', 'filters_changed'] const KNOWN_EVENTS = ['update', 'delete', 'notification', 'conversation', 'filters_changed', 'status.update']
export function processMessage (instanceName, timelineName, message) { export function processMessage (instanceName, timelineName, message) {
let { event, payload } = (message || {}) let { event, payload } = (message || {})
@ -12,7 +13,7 @@ export function processMessage (instanceName, timelineName, message) {
return return
} }
mark('processMessage') mark('processMessage')
if (['update', 'notification', 'conversation'].includes(event)) { if (['update', 'notification', 'conversation', 'status.update'].includes(event)) {
payload = JSON.parse(payload) // only these payloads are JSON-encoded for some reason payload = JSON.parse(payload) // only these payloads are JSON-encoded for some reason
} }
@ -43,6 +44,9 @@ export function processMessage (instanceName, timelineName, message) {
case 'filters_changed': case 'filters_changed':
emit('wordFiltersChanged', instanceName) emit('wordFiltersChanged', instanceName)
break break
case 'status.update':
updateStatus(instanceName, payload)
break
} }
stop('processMessage') stop('processMessage')
} }

View File

@ -0,0 +1,13 @@
import { database } from '../_database/database.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
async function doUpdateStatus (instanceName, newStatus) {
console.log('updating status', newStatus)
await database.updateStatus(instanceName, newStatus)
}
export function updateStatus (instanceName, newStatus) {
scheduleIdleTask(() => {
/* no await */ doUpdateStatus(instanceName, newStatus)
})
}

View File

@ -1,18 +1,20 @@
import { auth, basename } from './utils.js' import { auth, basename } from './utils.js'
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax.js' import { DEFAULT_TIMEOUT, get, post, put, WRITE_TIMEOUT } from '../_utils/ajax.js'
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds, // post is create, put is edit
async function postOrPutStatus (url, accessToken, method, text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll) { sensitive, spoilerText, visibility, poll) {
const url = `${basename(instanceName)}/api/v1/statuses`
const body = { const body = {
status: text, status: text,
in_reply_to_id: inReplyToId,
media_ids: mediaIds, media_ids: mediaIds,
sensitive, sensitive,
spoiler_text: spoilerText, spoiler_text: spoilerText,
visibility, poll,
poll ...(method === 'post' && {
// you can't change these properties when editing
in_reply_to_id: inReplyToId,
visibility
})
} }
for (const key of Object.keys(body)) { for (const key of Object.keys(body)) {
@ -23,7 +25,23 @@ export async function postStatus (instanceName, accessToken, text, inReplyToId,
} }
} }
return post(url, body, auth(accessToken), { timeout: WRITE_TIMEOUT }) const func = method === 'post' ? post : put
return func(url, body, auth(accessToken), { timeout: WRITE_TIMEOUT })
}
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll) {
const url = `${basename(instanceName)}/api/v1/statuses`
return postOrPutStatus(url, accessToken, 'post', text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll)
}
export async function putStatus (instanceName, accessToken, id, text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll) {
const url = `${basename(instanceName)}/api/v1/statuses/${id}`
return postOrPutStatus(url, accessToken, 'put', text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll)
} }
export async function getStatusContext (instanceName, accessToken, statusId) { export async function getStatusContext (instanceName, accessToken, statusId) {

View File

@ -3,12 +3,13 @@ import { getInCache, hasInCache, statusesCache } from '../cache.js'
import { STATUSES_STORE } from '../constants.js' import { STATUSES_STORE } from '../constants.js'
import { cacheStatus } from './cacheStatus.js' import { cacheStatus } from './cacheStatus.js'
import { putStatus } from './insertion.js' import { putStatus } from './insertion.js'
import { cloneForStorage } from '../helpers.js'
// //
// update statuses // update statuses
// //
async function updateStatus (instanceName, statusId, updateFunc) { async function doUpdateStatus (instanceName, statusId, updateFunc) {
const db = await getDatabase(instanceName) const db = await getDatabase(instanceName)
if (hasInCache(statusesCache, instanceName, statusId)) { if (hasInCache(statusesCache, instanceName, statusId)) {
const status = getInCache(statusesCache, instanceName, statusId) const status = getInCache(statusesCache, instanceName, statusId)
@ -25,7 +26,7 @@ async function updateStatus (instanceName, statusId, updateFunc) {
} }
export async function setStatusFavorited (instanceName, statusId, favorited) { export async function setStatusFavorited (instanceName, statusId, favorited) {
return updateStatus(instanceName, statusId, status => { return doUpdateStatus(instanceName, statusId, status => {
const delta = (favorited ? 1 : 0) - (status.favourited ? 1 : 0) const delta = (favorited ? 1 : 0) - (status.favourited ? 1 : 0)
status.favourited = favorited status.favourited = favorited
status.favourites_count = (status.favourites_count || 0) + delta status.favourites_count = (status.favourites_count || 0) + delta
@ -33,7 +34,7 @@ export async function setStatusFavorited (instanceName, statusId, favorited) {
} }
export async function setStatusReblogged (instanceName, statusId, reblogged) { export async function setStatusReblogged (instanceName, statusId, reblogged) {
return updateStatus(instanceName, statusId, status => { return doUpdateStatus(instanceName, statusId, status => {
const delta = (reblogged ? 1 : 0) - (status.reblogged ? 1 : 0) const delta = (reblogged ? 1 : 0) - (status.reblogged ? 1 : 0)
status.reblogged = reblogged status.reblogged = reblogged
status.reblogs_count = (status.reblogs_count || 0) + delta status.reblogs_count = (status.reblogs_count || 0) + delta
@ -41,19 +42,36 @@ export async function setStatusReblogged (instanceName, statusId, reblogged) {
} }
export async function setStatusPinned (instanceName, statusId, pinned) { export async function setStatusPinned (instanceName, statusId, pinned) {
return updateStatus(instanceName, statusId, status => { return doUpdateStatus(instanceName, statusId, status => {
status.pinned = pinned status.pinned = pinned
}) })
} }
export async function setStatusMuted (instanceName, statusId, muted) { export async function setStatusMuted (instanceName, statusId, muted) {
return updateStatus(instanceName, statusId, status => { return doUpdateStatus(instanceName, statusId, status => {
status.muted = muted status.muted = muted
}) })
} }
export async function setStatusBookmarked (instanceName, statusId, bookmarked) { export async function setStatusBookmarked (instanceName, statusId, bookmarked) {
return updateStatus(instanceName, statusId, status => { return doUpdateStatus(instanceName, statusId, status => {
status.bookmarked = bookmarked status.bookmarked = bookmarked
}) })
} }
// For the full list, see https://docs.joinmastodon.org/methods/statuses/#edit
const PROPS_THAT_CAN_BE_EDITED = ['content', 'spoiler_text', 'sensitive', 'language', 'media_ids', 'poll']
export async function updateStatus (instanceName, newStatus) {
const clonedNewStatus = cloneForStorage(newStatus)
return doUpdateStatus(instanceName, newStatus.id, status => {
// We can't use a simple Object.assign() to merge because a prop might have been deleted
for (const prop of PROPS_THAT_CAN_BE_EDITED) {
if (!(prop in clonedNewStatus)) {
delete status[prop]
} else {
status[prop] = clonedNewStatus[prop]
}
}
})
}

View File

@ -2,7 +2,7 @@ import { favoriteStatus } from '../src/routes/_api/favorite.js'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import FileApi from 'file-api' import FileApi from 'file-api'
import { users } from './users.js' import { users } from './users.js'
import { postStatus } from '../src/routes/_api/statuses.js' import { postStatus, putStatus } from '../src/routes/_api/statuses.js'
import { deleteStatus } from '../src/routes/_api/delete.js' import { deleteStatus } from '../src/routes/_api/delete.js'
import { authorizeFollowRequest, getFollowRequests } from '../src/routes/_api/followRequests.js' import { authorizeFollowRequest, getFollowRequests } from '../src/routes/_api/followRequests.js'
import { followAccount, unfollowAccount } from '../src/routes/_api/follow.js' import { followAccount, unfollowAccount } from '../src/routes/_api/follow.js'
@ -33,6 +33,11 @@ export async function postAs (username, text) {
null, null, false, null, 'public') null, null, false, null, 'public')
} }
export async function putAs (username, text, statusId) {
return putStatus(instanceName, users[username].accessToken, statusId, text,
null, null, false, null, 'public')
}
export async function postWithSpoilerAndPrivacyAs (username, text, spoiler, privacy) { export async function postWithSpoilerAndPrivacyAs (username, text, spoiler, privacy) {
return postStatus(instanceName, users[username].accessToken, text, return postStatus(instanceName, users[username].accessToken, text,
null, null, true, spoiler, privacy) null, null, true, spoiler, privacy)

28
tests/spec/140-editing.js Normal file
View File

@ -0,0 +1,28 @@
import {
getNthStatus, getUrl, goBack,
sleep
} from '../utils'
import { loginAsFoobar } from '../roles'
import { postAs, putAs } from '../serverActions'
fixture`140-editing.js`
.page`http://localhost:4002`
test('Edited toots are updated in the UI', async t => {
const { id: statusId } = await postAs('admin', 'yolo')
await sleep(500)
await loginAsFoobar(t)
await t.expect(getNthStatus(1).innerText).contains('yolo', { timeout: 20000 })
await putAs('admin', 'wait I mean YOLO', statusId)
await sleep(500)
await t.click(getNthStatus(1))
.expect(getUrl()).contains('/statuses')
.expect(getNthStatus(1).innerText).contains('wait I mean YOLO', { timeout: 20000 })
await goBack()
await t
.expect(getUrl()).eql('http://localhost:4002/')
.expect(getNthStatus(1).innerText).contains('wait I mean YOLO', { timeout: 20000 })
})