logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git

api.service.js (55114B)


  1. import { each, map, concat, last, get } from 'lodash'
  2. import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
  3. import { RegistrationError, StatusCodeError } from '../errors/errors'
  4. /* eslint-env browser */
  5. const MUTES_IMPORT_URL = '/api/pleroma/mutes_import'
  6. const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
  7. const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
  8. const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
  9. const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
  10. const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
  11. const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
  12. const ALIASES_URL = '/api/pleroma/aliases'
  13. const TAG_USER_URL = '/api/pleroma/admin/users/tag'
  14. const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
  15. const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
  16. const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
  17. const ADMIN_USERS_URL = '/api/pleroma/admin/users'
  18. const SUGGESTIONS_URL = '/api/v1/suggestions'
  19. const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
  20. const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
  21. const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
  22. const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
  23. const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp'
  24. const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp'
  25. const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp'
  26. const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
  27. const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
  28. const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
  29. const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
  30. const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss`
  31. const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
  32. const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
  33. const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
  34. const MASTODON_UNRETWEET_URL = id => `/api/v1/statuses/${id}/unreblog`
  35. const MASTODON_DELETE_URL = id => `/api/v1/statuses/${id}`
  36. const MASTODON_FOLLOW_URL = id => `/api/v1/accounts/${id}/follow`
  37. const MASTODON_UNFOLLOW_URL = id => `/api/v1/accounts/${id}/unfollow`
  38. const MASTODON_FOLLOWING_URL = id => `/api/v1/accounts/${id}/following`
  39. const MASTODON_FOLLOWERS_URL = id => `/api/v1/accounts/${id}/followers`
  40. const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests'
  41. const MASTODON_APPROVE_USER_URL = id => `/api/v1/follow_requests/${id}/authorize`
  42. const MASTODON_DENY_USER_URL = id => `/api/v1/follow_requests/${id}/reject`
  43. const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct'
  44. const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public'
  45. const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
  46. const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
  47. const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
  48. const MASTODON_STATUS_SOURCE_URL = id => `/api/v1/statuses/${id}/source`
  49. const MASTODON_STATUS_HISTORY_URL = id => `/api/v1/statuses/${id}/history`
  50. const MASTODON_USER_URL = '/api/v1/accounts'
  51. const MASTODON_USER_LOOKUP_URL = '/api/v1/accounts/lookup'
  52. const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
  53. const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
  54. const MASTODON_USER_IN_LISTS = id => `/api/v1/accounts/${id}/lists`
  55. const MASTODON_LIST_URL = id => `/api/v1/lists/${id}`
  56. const MASTODON_LIST_TIMELINE_URL = id => `/api/v1/timelines/list/${id}`
  57. const MASTODON_LIST_ACCOUNTS_URL = id => `/api/v1/lists/${id}/accounts`
  58. const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}`
  59. const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks'
  60. const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
  61. const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
  62. const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
  63. const MASTODON_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock`
  64. const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
  65. const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
  66. const MASTODON_REMOVE_USER_FROM_FOLLOWERS = id => `/api/v1/accounts/${id}/remove_from_followers`
  67. const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe`
  68. const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe`
  69. const MASTODON_USER_NOTE_URL = id => `/api/v1/accounts/${id}/note`
  70. const MASTODON_BOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/bookmark`
  71. const MASTODON_UNBOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/unbookmark`
  72. const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
  73. const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
  74. const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
  75. const MASTODON_POLL_URL = id => `/api/v1/polls/${id}`
  76. const MASTODON_STATUS_FAVORITEDBY_URL = id => `/api/v1/statuses/${id}/favourited_by`
  77. const MASTODON_STATUS_REBLOGGEDBY_URL = id => `/api/v1/statuses/${id}/reblogged_by`
  78. const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
  79. const MASTODON_REPORT_USER_URL = '/api/v1/reports'
  80. const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
  81. const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
  82. const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
  83. const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
  84. const MASTODON_SEARCH_2 = '/api/v2/search'
  85. const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
  86. const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
  87. const MASTODON_LISTS_URL = '/api/v1/lists'
  88. const MASTODON_STREAMING = '/api/v1/streaming'
  89. const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
  90. const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements'
  91. const MASTODON_ANNOUNCEMENTS_DISMISS_URL = id => `/api/v1/announcements/${id}/dismiss`
  92. const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
  93. const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
  94. const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
  95. const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats'
  96. const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
  97. const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
  98. const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
  99. const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
  100. const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports'
  101. const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
  102. const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
  103. const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
  104. const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
  105. const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
  106. const PLEROMA_SCROBBLES_URL = id => `/api/v1/pleroma/accounts/${id}/scrobbles`
  107. const PLEROMA_USER_FAVORITES_TIMELINE_URL = id => `/api/v1/pleroma/accounts/${id}/favourites`
  108. const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config'
  109. const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions'
  110. const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends'
  111. const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install'
  112. const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
  113. const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
  114. const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) => `/api/v1/pleroma/emoji/packs?page=${page}&page_size=${pageSize}`
  115. const PLEROMA_EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}`
  116. const PLEROMA_EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download'
  117. const PLEROMA_EMOJI_PACKS_LS_REMOTE_URL =
  118. (url, page, pageSize) => `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}`
  119. const PLEROMA_EMOJI_UPDATE_FILE_URL = (name) => `/api/v1/pleroma/emoji/packs/files?name=${name}`
  120. const oldfetch = window.fetch
  121. const fetch = (url, options) => {
  122. options = options || {}
  123. const baseUrl = ''
  124. const fullUrl = baseUrl + url
  125. options.credentials = 'same-origin'
  126. return oldfetch(fullUrl, options)
  127. }
  128. const promisedRequest = ({ method, url, params, payload, credentials, headers = {} }) => {
  129. const options = {
  130. method,
  131. headers: {
  132. Accept: 'application/json',
  133. 'Content-Type': 'application/json',
  134. ...headers
  135. }
  136. }
  137. if (params) {
  138. url += '?' + Object.entries(params)
  139. .map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value))
  140. .join('&')
  141. }
  142. if (payload) {
  143. options.body = JSON.stringify(payload)
  144. }
  145. if (credentials) {
  146. options.headers = {
  147. ...options.headers,
  148. ...authHeaders(credentials)
  149. }
  150. }
  151. return fetch(url, options)
  152. .then((response) => {
  153. return new Promise((resolve, reject) => response.json()
  154. .then((json) => {
  155. if (!response.ok) {
  156. return reject(new StatusCodeError(response.status, json, { url, options }, response))
  157. }
  158. return resolve(json)
  159. })
  160. .catch((error) => {
  161. return reject(new StatusCodeError(response.status, error, { url, options }, response))
  162. })
  163. )
  164. })
  165. }
  166. const updateNotificationSettings = ({ credentials, settings }) => {
  167. const form = new FormData()
  168. each(settings, (value, key) => {
  169. form.append(key, value)
  170. })
  171. return fetch(`${NOTIFICATION_SETTINGS_URL}?${new URLSearchParams(settings)}`, {
  172. headers: authHeaders(credentials),
  173. method: 'PUT',
  174. body: form
  175. }).then((data) => data.json())
  176. }
  177. const updateProfileImages = ({ credentials, avatar = null, avatarName = null, banner = null, background = null }) => {
  178. const form = new FormData()
  179. if (avatar !== null) {
  180. if (avatarName !== null) {
  181. form.append('avatar', avatar, avatarName)
  182. } else {
  183. form.append('avatar', avatar)
  184. }
  185. }
  186. if (banner !== null) form.append('header', banner)
  187. if (background !== null) form.append('pleroma_background_image', background)
  188. return fetch(MASTODON_PROFILE_UPDATE_URL, {
  189. headers: authHeaders(credentials),
  190. method: 'PATCH',
  191. body: form
  192. })
  193. .then((data) => data.json())
  194. .then((data) => {
  195. if (data.error) {
  196. throw new Error(data.error)
  197. }
  198. return parseUser(data)
  199. })
  200. }
  201. const updateProfile = ({ credentials, params }) => {
  202. return promisedRequest({
  203. url: MASTODON_PROFILE_UPDATE_URL,
  204. method: 'PATCH',
  205. payload: params,
  206. credentials
  207. }).then((data) => parseUser(data))
  208. }
  209. // Params needed:
  210. // nickname
  211. // email
  212. // fullname
  213. // password
  214. // password_confirm
  215. //
  216. // Optional
  217. // bio
  218. // homepage
  219. // location
  220. // token
  221. // language
  222. const register = ({ params, credentials }) => {
  223. const { nickname, ...rest } = params
  224. return fetch(MASTODON_REGISTRATION_URL, {
  225. method: 'POST',
  226. headers: {
  227. ...authHeaders(credentials),
  228. 'Content-Type': 'application/json'
  229. },
  230. body: JSON.stringify({
  231. nickname,
  232. locale: 'en_US',
  233. agreement: true,
  234. ...rest
  235. })
  236. })
  237. .then((response) => {
  238. if (response.ok) {
  239. return response.json()
  240. } else {
  241. return response.json().then((error) => { throw new RegistrationError(error) })
  242. }
  243. })
  244. }
  245. const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json())
  246. const authHeaders = (accessToken) => {
  247. if (accessToken) {
  248. return { Authorization: `Bearer ${accessToken}` }
  249. } else {
  250. return { }
  251. }
  252. }
  253. const followUser = ({ id, credentials, ...options }) => {
  254. const url = MASTODON_FOLLOW_URL(id)
  255. const form = {}
  256. if (options.reblogs !== undefined) { form.reblogs = options.reblogs }
  257. return fetch(url, {
  258. body: JSON.stringify(form),
  259. headers: {
  260. ...authHeaders(credentials),
  261. 'Content-Type': 'application/json'
  262. },
  263. method: 'POST'
  264. }).then((data) => data.json())
  265. }
  266. const unfollowUser = ({ id, credentials }) => {
  267. const url = MASTODON_UNFOLLOW_URL(id)
  268. return fetch(url, {
  269. headers: authHeaders(credentials),
  270. method: 'POST'
  271. }).then((data) => data.json())
  272. }
  273. const fetchUserInLists = ({ id, credentials }) => {
  274. const url = MASTODON_USER_IN_LISTS(id)
  275. return fetch(url, {
  276. headers: authHeaders(credentials)
  277. }).then((data) => data.json())
  278. }
  279. const pinOwnStatus = ({ id, credentials }) => {
  280. return promisedRequest({ url: MASTODON_PIN_OWN_STATUS(id), credentials, method: 'POST' })
  281. .then((data) => parseStatus(data))
  282. }
  283. const unpinOwnStatus = ({ id, credentials }) => {
  284. return promisedRequest({ url: MASTODON_UNPIN_OWN_STATUS(id), credentials, method: 'POST' })
  285. .then((data) => parseStatus(data))
  286. }
  287. const muteConversation = ({ id, credentials }) => {
  288. return promisedRequest({ url: MASTODON_MUTE_CONVERSATION(id), credentials, method: 'POST' })
  289. .then((data) => parseStatus(data))
  290. }
  291. const unmuteConversation = ({ id, credentials }) => {
  292. return promisedRequest({ url: MASTODON_UNMUTE_CONVERSATION(id), credentials, method: 'POST' })
  293. .then((data) => parseStatus(data))
  294. }
  295. const blockUser = ({ id, credentials }) => {
  296. return fetch(MASTODON_BLOCK_USER_URL(id), {
  297. headers: authHeaders(credentials),
  298. method: 'POST'
  299. }).then((data) => data.json())
  300. }
  301. const unblockUser = ({ id, credentials }) => {
  302. return fetch(MASTODON_UNBLOCK_USER_URL(id), {
  303. headers: authHeaders(credentials),
  304. method: 'POST'
  305. }).then((data) => data.json())
  306. }
  307. const removeUserFromFollowers = ({ id, credentials }) => {
  308. return fetch(MASTODON_REMOVE_USER_FROM_FOLLOWERS(id), {
  309. headers: authHeaders(credentials),
  310. method: 'POST'
  311. }).then((data) => data.json())
  312. }
  313. const editUserNote = ({ id, credentials, comment }) => {
  314. return promisedRequest({
  315. url: MASTODON_USER_NOTE_URL(id),
  316. credentials,
  317. payload: {
  318. comment
  319. },
  320. method: 'POST'
  321. })
  322. }
  323. const approveUser = ({ id, credentials }) => {
  324. const url = MASTODON_APPROVE_USER_URL(id)
  325. return fetch(url, {
  326. headers: authHeaders(credentials),
  327. method: 'POST'
  328. }).then((data) => data.json())
  329. }
  330. const denyUser = ({ id, credentials }) => {
  331. const url = MASTODON_DENY_USER_URL(id)
  332. return fetch(url, {
  333. headers: authHeaders(credentials),
  334. method: 'POST'
  335. }).then((data) => data.json())
  336. }
  337. const fetchUser = ({ id, credentials }) => {
  338. const url = `${MASTODON_USER_URL}/${id}`
  339. return promisedRequest({ url, credentials })
  340. .then((data) => parseUser(data))
  341. }
  342. const fetchUserByName = ({ name, credentials }) => {
  343. return promisedRequest({
  344. url: MASTODON_USER_LOOKUP_URL,
  345. credentials,
  346. params: { acct: name }
  347. })
  348. .then(data => data.id)
  349. .catch(error => {
  350. if (error && error.statusCode === 404) {
  351. // Either the backend does not support lookup endpoint,
  352. // or there is no user with such name. Fallback and treat name as id.
  353. return name
  354. } else {
  355. throw error
  356. }
  357. })
  358. .then(id => fetchUser({ id, credentials }))
  359. }
  360. const fetchUserRelationship = ({ id, credentials }) => {
  361. const url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
  362. return fetch(url, { headers: authHeaders(credentials) })
  363. .then((response) => {
  364. return new Promise((resolve, reject) => response.json()
  365. .then((json) => {
  366. if (!response.ok) {
  367. return reject(new StatusCodeError(response.status, json, { url }, response))
  368. }
  369. return resolve(json)
  370. }))
  371. })
  372. }
  373. const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => {
  374. let url = MASTODON_FOLLOWING_URL(id)
  375. const args = [
  376. maxId && `max_id=${maxId}`,
  377. sinceId && `since_id=${sinceId}`,
  378. limit && `limit=${limit}`,
  379. 'with_relationships=true'
  380. ].filter(_ => _).join('&')
  381. url = url + (args ? '?' + args : '')
  382. return fetch(url, { headers: authHeaders(credentials) })
  383. .then((data) => data.json())
  384. .then((data) => data.map(parseUser))
  385. }
  386. const exportFriends = ({ id, credentials }) => {
  387. // eslint-disable-next-line no-async-promise-executor
  388. return new Promise(async (resolve, reject) => {
  389. try {
  390. let friends = []
  391. let more = true
  392. while (more) {
  393. const maxId = friends.length > 0 ? last(friends).id : undefined
  394. const users = await fetchFriends({ id, maxId, credentials })
  395. friends = concat(friends, users)
  396. if (users.length === 0) {
  397. more = false
  398. }
  399. }
  400. resolve(friends)
  401. } catch (err) {
  402. reject(err)
  403. }
  404. })
  405. }
  406. const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => {
  407. let url = MASTODON_FOLLOWERS_URL(id)
  408. const args = [
  409. maxId && `max_id=${maxId}`,
  410. sinceId && `since_id=${sinceId}`,
  411. limit && `limit=${limit}`,
  412. 'with_relationships=true'
  413. ].filter(_ => _).join('&')
  414. url += args ? '?' + args : ''
  415. return fetch(url, { headers: authHeaders(credentials) })
  416. .then((data) => data.json())
  417. .then((data) => data.map(parseUser))
  418. }
  419. const fetchFollowRequests = ({ credentials }) => {
  420. const url = MASTODON_FOLLOW_REQUESTS_URL
  421. return fetch(url, { headers: authHeaders(credentials) })
  422. .then((data) => data.json())
  423. .then((data) => data.map(parseUser))
  424. }
  425. const fetchLists = ({ credentials }) => {
  426. const url = MASTODON_LISTS_URL
  427. return fetch(url, { headers: authHeaders(credentials) })
  428. .then((data) => data.json())
  429. }
  430. const createList = ({ title, credentials }) => {
  431. const url = MASTODON_LISTS_URL
  432. const headers = authHeaders(credentials)
  433. headers['Content-Type'] = 'application/json'
  434. return fetch(url, {
  435. headers,
  436. method: 'POST',
  437. body: JSON.stringify({ title })
  438. }).then((data) => data.json())
  439. }
  440. const getList = ({ listId, credentials }) => {
  441. const url = MASTODON_LIST_URL(listId)
  442. return fetch(url, { headers: authHeaders(credentials) })
  443. .then((data) => data.json())
  444. }
  445. const updateList = ({ listId, title, credentials }) => {
  446. const url = MASTODON_LIST_URL(listId)
  447. const headers = authHeaders(credentials)
  448. headers['Content-Type'] = 'application/json'
  449. return fetch(url, {
  450. headers,
  451. method: 'PUT',
  452. body: JSON.stringify({ title })
  453. })
  454. }
  455. const getListAccounts = ({ listId, credentials }) => {
  456. const url = MASTODON_LIST_ACCOUNTS_URL(listId)
  457. return fetch(url, { headers: authHeaders(credentials) })
  458. .then((data) => data.json())
  459. .then((data) => data.map(({ id }) => id))
  460. }
  461. const addAccountsToList = ({ listId, accountIds, credentials }) => {
  462. const url = MASTODON_LIST_ACCOUNTS_URL(listId)
  463. const headers = authHeaders(credentials)
  464. headers['Content-Type'] = 'application/json'
  465. return fetch(url, {
  466. headers,
  467. method: 'POST',
  468. body: JSON.stringify({ account_ids: accountIds })
  469. })
  470. }
  471. const removeAccountsFromList = ({ listId, accountIds, credentials }) => {
  472. const url = MASTODON_LIST_ACCOUNTS_URL(listId)
  473. const headers = authHeaders(credentials)
  474. headers['Content-Type'] = 'application/json'
  475. return fetch(url, {
  476. headers,
  477. method: 'DELETE',
  478. body: JSON.stringify({ account_ids: accountIds })
  479. })
  480. }
  481. const deleteList = ({ listId, credentials }) => {
  482. const url = MASTODON_LIST_URL(listId)
  483. return fetch(url, {
  484. method: 'DELETE',
  485. headers: authHeaders(credentials)
  486. })
  487. }
  488. const fetchConversation = ({ id, credentials }) => {
  489. const urlContext = MASTODON_STATUS_CONTEXT_URL(id)
  490. return fetch(urlContext, { headers: authHeaders(credentials) })
  491. .then((data) => {
  492. if (data.ok) {
  493. return data
  494. }
  495. throw new Error('Error fetching timeline', data)
  496. })
  497. .then((data) => data.json())
  498. .then(({ ancestors, descendants }) => ({
  499. ancestors: ancestors.map(parseStatus),
  500. descendants: descendants.map(parseStatus)
  501. }))
  502. }
  503. const fetchStatus = ({ id, credentials }) => {
  504. const url = MASTODON_STATUS_URL(id)
  505. return fetch(url, { headers: authHeaders(credentials) })
  506. .then((data) => {
  507. if (data.ok) {
  508. return data
  509. }
  510. throw new Error('Error fetching timeline', data)
  511. })
  512. .then((data) => data.json())
  513. .then((data) => parseStatus(data))
  514. }
  515. const fetchStatusSource = ({ id, credentials }) => {
  516. const url = MASTODON_STATUS_SOURCE_URL(id)
  517. return fetch(url, { headers: authHeaders(credentials) })
  518. .then((data) => {
  519. if (data.ok) {
  520. return data
  521. }
  522. throw new Error('Error fetching source', data)
  523. })
  524. .then((data) => data.json())
  525. .then((data) => parseSource(data))
  526. }
  527. const fetchStatusHistory = ({ status, credentials }) => {
  528. const url = MASTODON_STATUS_HISTORY_URL(status.id)
  529. return promisedRequest({ url, credentials })
  530. .then((data) => {
  531. data.reverse()
  532. return data.map((item) => {
  533. item.originalStatus = status
  534. return parseStatus(item)
  535. })
  536. })
  537. }
  538. const tagUser = ({ tag, credentials, user }) => {
  539. const screenName = user.screen_name
  540. const form = {
  541. nicknames: [screenName],
  542. tags: [tag]
  543. }
  544. const headers = authHeaders(credentials)
  545. headers['Content-Type'] = 'application/json'
  546. return fetch(TAG_USER_URL, {
  547. method: 'PUT',
  548. headers,
  549. body: JSON.stringify(form)
  550. })
  551. }
  552. const untagUser = ({ tag, credentials, user }) => {
  553. const screenName = user.screen_name
  554. const body = {
  555. nicknames: [screenName],
  556. tags: [tag]
  557. }
  558. const headers = authHeaders(credentials)
  559. headers['Content-Type'] = 'application/json'
  560. return fetch(TAG_USER_URL, {
  561. method: 'DELETE',
  562. headers,
  563. body: JSON.stringify(body)
  564. })
  565. }
  566. const addRight = ({ right, credentials, user }) => {
  567. const screenName = user.screen_name
  568. return fetch(PERMISSION_GROUP_URL(screenName, right), {
  569. method: 'POST',
  570. headers: authHeaders(credentials),
  571. body: {}
  572. })
  573. }
  574. const deleteRight = ({ right, credentials, user }) => {
  575. const screenName = user.screen_name
  576. return fetch(PERMISSION_GROUP_URL(screenName, right), {
  577. method: 'DELETE',
  578. headers: authHeaders(credentials),
  579. body: {}
  580. })
  581. }
  582. const activateUser = ({ credentials, user: { screen_name: nickname } }) => {
  583. return promisedRequest({
  584. url: ACTIVATE_USER_URL,
  585. method: 'PATCH',
  586. credentials,
  587. payload: {
  588. nicknames: [nickname]
  589. }
  590. }).then(response => get(response, 'users.0'))
  591. }
  592. const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
  593. return promisedRequest({
  594. url: DEACTIVATE_USER_URL,
  595. method: 'PATCH',
  596. credentials,
  597. payload: {
  598. nicknames: [nickname]
  599. }
  600. }).then(response => get(response, 'users.0'))
  601. }
  602. const deleteUser = ({ credentials, user }) => {
  603. const screenName = user.screen_name
  604. const headers = authHeaders(credentials)
  605. return fetch(`${ADMIN_USERS_URL}?nickname=${screenName}`, {
  606. method: 'DELETE',
  607. headers
  608. })
  609. }
  610. const fetchTimeline = ({
  611. timeline,
  612. credentials,
  613. since = false,
  614. minId = false,
  615. until = false,
  616. userId = false,
  617. listId = false,
  618. tag = false,
  619. withMuted = false,
  620. replyVisibility = 'all',
  621. includeTypes = []
  622. }) => {
  623. const timelineUrls = {
  624. public: MASTODON_PUBLIC_TIMELINE,
  625. friends: MASTODON_USER_HOME_TIMELINE_URL,
  626. dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
  627. notifications: MASTODON_USER_NOTIFICATIONS_URL,
  628. publicAndExternal: MASTODON_PUBLIC_TIMELINE,
  629. user: MASTODON_USER_TIMELINE_URL,
  630. media: MASTODON_USER_TIMELINE_URL,
  631. list: MASTODON_LIST_TIMELINE_URL,
  632. favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
  633. publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL,
  634. tag: MASTODON_TAG_TIMELINE_URL,
  635. bookmarks: MASTODON_BOOKMARK_TIMELINE_URL
  636. }
  637. const isNotifications = timeline === 'notifications'
  638. const params = []
  639. let url = timelineUrls[timeline]
  640. if (timeline === 'favorites' && userId) {
  641. url = timelineUrls.publicFavorites(userId)
  642. }
  643. if (timeline === 'user' || timeline === 'media') {
  644. url = url(userId)
  645. }
  646. if (timeline === 'list') {
  647. url = url(listId)
  648. }
  649. if (minId) {
  650. params.push(['min_id', minId])
  651. }
  652. if (since) {
  653. params.push(['since_id', since])
  654. }
  655. if (until) {
  656. params.push(['max_id', until])
  657. }
  658. if (tag) {
  659. url = url(tag)
  660. }
  661. if (timeline === 'media') {
  662. params.push(['only_media', 1])
  663. }
  664. if (timeline === 'public') {
  665. params.push(['local', true])
  666. }
  667. if (timeline === 'public' || timeline === 'publicAndExternal') {
  668. params.push(['only_media', false])
  669. }
  670. if (timeline !== 'favorites' && timeline !== 'bookmarks') {
  671. params.push(['with_muted', withMuted])
  672. }
  673. if (replyVisibility !== 'all') {
  674. params.push(['reply_visibility', replyVisibility])
  675. }
  676. if (includeTypes.length > 0) {
  677. includeTypes.forEach(type => {
  678. params.push(['include_types[]', type])
  679. })
  680. }
  681. params.push(['limit', 20])
  682. const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
  683. url += `?${queryString}`
  684. return fetch(url, { headers: authHeaders(credentials) })
  685. .then(async (response) => {
  686. const success = response.ok
  687. const data = await response.json()
  688. if (success && !data.errors) {
  689. const pagination = parseLinkHeaderPagination(response.headers.get('Link'), {
  690. flakeId: timeline !== 'bookmarks' && timeline !== 'notifications'
  691. })
  692. return { data: data.map(isNotifications ? parseNotification : parseStatus), pagination }
  693. } else {
  694. data.errors ||= []
  695. data.status = response.status
  696. data.statusText = response.statusText
  697. return data
  698. }
  699. })
  700. }
  701. const fetchPinnedStatuses = ({ id, credentials }) => {
  702. const url = MASTODON_USER_TIMELINE_URL(id) + '?pinned=true'
  703. return promisedRequest({ url, credentials })
  704. .then((data) => data.map(parseStatus))
  705. }
  706. const verifyCredentials = (user) => {
  707. return fetch(MASTODON_LOGIN_URL, {
  708. headers: authHeaders(user)
  709. })
  710. .then((response) => {
  711. if (response.ok) {
  712. return response.json()
  713. } else {
  714. return {
  715. error: response
  716. }
  717. }
  718. })
  719. .then((data) => data.error ? data : parseUser(data))
  720. }
  721. const favorite = ({ id, credentials }) => {
  722. return promisedRequest({ url: MASTODON_FAVORITE_URL(id), method: 'POST', credentials })
  723. .then((data) => parseStatus(data))
  724. }
  725. const unfavorite = ({ id, credentials }) => {
  726. return promisedRequest({ url: MASTODON_UNFAVORITE_URL(id), method: 'POST', credentials })
  727. .then((data) => parseStatus(data))
  728. }
  729. const retweet = ({ id, credentials }) => {
  730. return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', credentials })
  731. .then((data) => parseStatus(data))
  732. }
  733. const unretweet = ({ id, credentials }) => {
  734. return promisedRequest({ url: MASTODON_UNRETWEET_URL(id), method: 'POST', credentials })
  735. .then((data) => parseStatus(data))
  736. }
  737. const bookmarkStatus = ({ id, credentials }) => {
  738. return promisedRequest({
  739. url: MASTODON_BOOKMARK_STATUS_URL(id),
  740. headers: authHeaders(credentials),
  741. method: 'POST'
  742. })
  743. }
  744. const unbookmarkStatus = ({ id, credentials }) => {
  745. return promisedRequest({
  746. url: MASTODON_UNBOOKMARK_STATUS_URL(id),
  747. headers: authHeaders(credentials),
  748. method: 'POST'
  749. })
  750. }
  751. const postStatus = ({
  752. credentials,
  753. status,
  754. spoilerText,
  755. visibility,
  756. sensitive,
  757. poll,
  758. mediaIds = [],
  759. inReplyToStatusId,
  760. quoteId,
  761. contentType,
  762. preview,
  763. idempotencyKey
  764. }) => {
  765. const form = new FormData()
  766. const pollOptions = poll.options || []
  767. form.append('status', status)
  768. form.append('source', 'Pleroma FE')
  769. if (spoilerText) form.append('spoiler_text', spoilerText)
  770. if (visibility) form.append('visibility', visibility)
  771. if (sensitive) form.append('sensitive', sensitive)
  772. if (contentType) form.append('content_type', contentType)
  773. mediaIds.forEach(val => {
  774. form.append('media_ids[]', val)
  775. })
  776. if (pollOptions.some(option => option !== '')) {
  777. const normalizedPoll = {
  778. expires_in: parseInt(poll.expiresIn, 10),
  779. multiple: poll.multiple
  780. }
  781. Object.keys(normalizedPoll).forEach(key => {
  782. form.append(`poll[${key}]`, normalizedPoll[key])
  783. })
  784. pollOptions.forEach(option => {
  785. form.append('poll[options][]', option)
  786. })
  787. }
  788. if (inReplyToStatusId) {
  789. form.append('in_reply_to_id', inReplyToStatusId)
  790. }
  791. if (quoteId) {
  792. form.append('quote_id', quoteId)
  793. }
  794. if (preview) {
  795. form.append('preview', 'true')
  796. }
  797. const postHeaders = authHeaders(credentials)
  798. if (idempotencyKey) {
  799. postHeaders['idempotency-key'] = idempotencyKey
  800. }
  801. return fetch(MASTODON_POST_STATUS_URL, {
  802. body: form,
  803. method: 'POST',
  804. headers: postHeaders
  805. })
  806. .then((response) => {
  807. return response.json()
  808. })
  809. .then((data) => data.error ? data : parseStatus(data))
  810. }
  811. const editStatus = ({
  812. id,
  813. credentials,
  814. status,
  815. spoilerText,
  816. sensitive,
  817. poll,
  818. mediaIds = [],
  819. contentType
  820. }) => {
  821. const form = new FormData()
  822. const pollOptions = poll.options || []
  823. form.append('status', status)
  824. if (spoilerText) form.append('spoiler_text', spoilerText)
  825. if (sensitive) form.append('sensitive', sensitive)
  826. if (contentType) form.append('content_type', contentType)
  827. mediaIds.forEach(val => {
  828. form.append('media_ids[]', val)
  829. })
  830. if (pollOptions.some(option => option !== '')) {
  831. const normalizedPoll = {
  832. expires_in: parseInt(poll.expiresIn, 10),
  833. multiple: poll.multiple
  834. }
  835. Object.keys(normalizedPoll).forEach(key => {
  836. form.append(`poll[${key}]`, normalizedPoll[key])
  837. })
  838. pollOptions.forEach(option => {
  839. form.append('poll[options][]', option)
  840. })
  841. }
  842. const putHeaders = authHeaders(credentials)
  843. return fetch(MASTODON_STATUS_URL(id), {
  844. body: form,
  845. method: 'PUT',
  846. headers: putHeaders
  847. })
  848. .then((response) => {
  849. return response.json()
  850. })
  851. .then((data) => data.error ? data : parseStatus(data))
  852. }
  853. const deleteStatus = ({ id, credentials }) => {
  854. return promisedRequest({
  855. url: MASTODON_DELETE_URL(id),
  856. credentials,
  857. method: 'DELETE'
  858. })
  859. }
  860. const uploadMedia = ({ formData, credentials }) => {
  861. return fetch(MASTODON_MEDIA_UPLOAD_URL, {
  862. body: formData,
  863. method: 'POST',
  864. headers: authHeaders(credentials)
  865. })
  866. .then((data) => data.json())
  867. .then((data) => parseAttachment(data))
  868. }
  869. const setMediaDescription = ({ id, description, credentials }) => {
  870. return promisedRequest({
  871. url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`,
  872. method: 'PUT',
  873. headers: authHeaders(credentials),
  874. payload: {
  875. description
  876. }
  877. }).then((data) => parseAttachment(data))
  878. }
  879. const importMutes = ({ file, credentials }) => {
  880. const formData = new FormData()
  881. formData.append('list', file)
  882. return fetch(MUTES_IMPORT_URL, {
  883. body: formData,
  884. method: 'POST',
  885. headers: authHeaders(credentials)
  886. })
  887. .then((response) => response.ok)
  888. }
  889. const importBlocks = ({ file, credentials }) => {
  890. const formData = new FormData()
  891. formData.append('list', file)
  892. return fetch(BLOCKS_IMPORT_URL, {
  893. body: formData,
  894. method: 'POST',
  895. headers: authHeaders(credentials)
  896. })
  897. .then((response) => response.ok)
  898. }
  899. const importFollows = ({ file, credentials }) => {
  900. const formData = new FormData()
  901. formData.append('list', file)
  902. return fetch(FOLLOW_IMPORT_URL, {
  903. body: formData,
  904. method: 'POST',
  905. headers: authHeaders(credentials)
  906. })
  907. .then((response) => response.ok)
  908. }
  909. const deleteAccount = ({ credentials, password }) => {
  910. const form = new FormData()
  911. form.append('password', password)
  912. return fetch(DELETE_ACCOUNT_URL, {
  913. body: form,
  914. method: 'POST',
  915. headers: authHeaders(credentials)
  916. })
  917. .then((response) => response.json())
  918. }
  919. const changeEmail = ({ credentials, email, password }) => {
  920. const form = new FormData()
  921. form.append('email', email)
  922. form.append('password', password)
  923. return fetch(CHANGE_EMAIL_URL, {
  924. body: form,
  925. method: 'POST',
  926. headers: authHeaders(credentials)
  927. })
  928. .then((response) => response.json())
  929. }
  930. const moveAccount = ({ credentials, password, targetAccount }) => {
  931. const form = new FormData()
  932. form.append('password', password)
  933. form.append('target_account', targetAccount)
  934. return fetch(MOVE_ACCOUNT_URL, {
  935. body: form,
  936. method: 'POST',
  937. headers: authHeaders(credentials)
  938. })
  939. .then((response) => response.json())
  940. }
  941. const addAlias = ({ credentials, alias }) => {
  942. return promisedRequest({
  943. url: ALIASES_URL,
  944. method: 'PUT',
  945. credentials,
  946. payload: { alias }
  947. })
  948. }
  949. const deleteAlias = ({ credentials, alias }) => {
  950. return promisedRequest({
  951. url: ALIASES_URL,
  952. method: 'DELETE',
  953. credentials,
  954. payload: { alias }
  955. })
  956. }
  957. const listAliases = ({ credentials }) => {
  958. return promisedRequest({
  959. url: ALIASES_URL,
  960. method: 'GET',
  961. credentials,
  962. params: {
  963. _cacheBooster: (new Date()).getTime()
  964. }
  965. })
  966. }
  967. const changePassword = ({ credentials, password, newPassword, newPasswordConfirmation }) => {
  968. const form = new FormData()
  969. form.append('password', password)
  970. form.append('new_password', newPassword)
  971. form.append('new_password_confirmation', newPasswordConfirmation)
  972. return fetch(CHANGE_PASSWORD_URL, {
  973. body: form,
  974. method: 'POST',
  975. headers: authHeaders(credentials)
  976. })
  977. .then((response) => response.json())
  978. }
  979. const settingsMFA = ({ credentials }) => {
  980. return fetch(MFA_SETTINGS_URL, {
  981. headers: authHeaders(credentials),
  982. method: 'GET'
  983. }).then((data) => data.json())
  984. }
  985. const mfaDisableOTP = ({ credentials, password }) => {
  986. const form = new FormData()
  987. form.append('password', password)
  988. return fetch(MFA_DISABLE_OTP_URL, {
  989. body: form,
  990. method: 'DELETE',
  991. headers: authHeaders(credentials)
  992. })
  993. .then((response) => response.json())
  994. }
  995. const mfaConfirmOTP = ({ credentials, password, token }) => {
  996. const form = new FormData()
  997. form.append('password', password)
  998. form.append('code', token)
  999. return fetch(MFA_CONFIRM_OTP_URL, {
  1000. body: form,
  1001. headers: authHeaders(credentials),
  1002. method: 'POST'
  1003. }).then((data) => data.json())
  1004. }
  1005. const mfaSetupOTP = ({ credentials }) => {
  1006. return fetch(MFA_SETUP_OTP_URL, {
  1007. headers: authHeaders(credentials),
  1008. method: 'GET'
  1009. }).then((data) => data.json())
  1010. }
  1011. const generateMfaBackupCodes = ({ credentials }) => {
  1012. return fetch(MFA_BACKUP_CODES_URL, {
  1013. headers: authHeaders(credentials),
  1014. method: 'GET'
  1015. }).then((data) => data.json())
  1016. }
  1017. const fetchMutes = ({ maxId, credentials }) => {
  1018. const query = new URLSearchParams({ with_relationships: true })
  1019. if (maxId) {
  1020. query.append('max_id', maxId)
  1021. }
  1022. return promisedRequest({ url: `${MASTODON_USER_MUTES_URL}?${query.toString()}`, credentials })
  1023. .then((users) => users.map(parseUser))
  1024. }
  1025. const muteUser = ({ id, expiresIn, credentials }) => {
  1026. const payload = {}
  1027. if (expiresIn) {
  1028. payload.expires_in = expiresIn
  1029. }
  1030. return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST', payload })
  1031. }
  1032. const unmuteUser = ({ id, credentials }) => {
  1033. return promisedRequest({ url: MASTODON_UNMUTE_USER_URL(id), credentials, method: 'POST' })
  1034. }
  1035. const subscribeUser = ({ id, credentials }) => {
  1036. return promisedRequest({ url: MASTODON_SUBSCRIBE_USER(id), credentials, method: 'POST' })
  1037. }
  1038. const unsubscribeUser = ({ id, credentials }) => {
  1039. return promisedRequest({ url: MASTODON_UNSUBSCRIBE_USER(id), credentials, method: 'POST' })
  1040. }
  1041. const fetchBlocks = ({ maxId, credentials }) => {
  1042. const query = new URLSearchParams({ with_relationships: true })
  1043. if (maxId) {
  1044. query.append('max_id', maxId)
  1045. }
  1046. return promisedRequest({ url: `${MASTODON_USER_BLOCKS_URL}?${query.toString()}`, credentials })
  1047. .then((users) => users.map(parseUser))
  1048. }
  1049. const addBackup = ({ credentials }) => {
  1050. return promisedRequest({
  1051. url: PLEROMA_BACKUP_URL,
  1052. method: 'POST',
  1053. credentials
  1054. })
  1055. }
  1056. const listBackups = ({ credentials }) => {
  1057. return promisedRequest({
  1058. url: PLEROMA_BACKUP_URL,
  1059. method: 'GET',
  1060. credentials,
  1061. params: {
  1062. _cacheBooster: (new Date()).getTime()
  1063. }
  1064. })
  1065. }
  1066. const fetchOAuthTokens = ({ credentials }) => {
  1067. const url = '/api/oauth_tokens.json'
  1068. return fetch(url, {
  1069. headers: authHeaders(credentials)
  1070. }).then((data) => {
  1071. if (data.ok) {
  1072. return data.json()
  1073. }
  1074. throw new Error('Error fetching auth tokens', data)
  1075. })
  1076. }
  1077. const revokeOAuthToken = ({ id, credentials }) => {
  1078. const url = `/api/oauth_tokens/${id}`
  1079. return fetch(url, {
  1080. headers: authHeaders(credentials),
  1081. method: 'DELETE'
  1082. })
  1083. }
  1084. const suggestions = ({ credentials }) => {
  1085. return fetch(SUGGESTIONS_URL, {
  1086. headers: authHeaders(credentials)
  1087. }).then((data) => data.json())
  1088. }
  1089. const markNotificationsAsSeen = ({ id, credentials, single = false }) => {
  1090. const body = new FormData()
  1091. if (single) {
  1092. body.append('id', id)
  1093. } else {
  1094. body.append('max_id', id)
  1095. }
  1096. return fetch(NOTIFICATION_READ_URL, {
  1097. body,
  1098. headers: authHeaders(credentials),
  1099. method: 'POST'
  1100. }).then((data) => data.json())
  1101. }
  1102. const vote = ({ pollId, choices, credentials }) => {
  1103. const form = new FormData()
  1104. form.append('choices', choices)
  1105. return promisedRequest({
  1106. url: MASTODON_VOTE_URL(encodeURIComponent(pollId)),
  1107. method: 'POST',
  1108. credentials,
  1109. payload: {
  1110. choices
  1111. }
  1112. })
  1113. }
  1114. const fetchPoll = ({ pollId, credentials }) => {
  1115. return promisedRequest(
  1116. {
  1117. url: MASTODON_POLL_URL(encodeURIComponent(pollId)),
  1118. method: 'GET',
  1119. credentials
  1120. }
  1121. )
  1122. }
  1123. const fetchFavoritedByUsers = ({ id, credentials }) => {
  1124. return promisedRequest({
  1125. url: MASTODON_STATUS_FAVORITEDBY_URL(id),
  1126. method: 'GET',
  1127. credentials
  1128. }).then((users) => users.map(parseUser))
  1129. }
  1130. const fetchRebloggedByUsers = ({ id, credentials }) => {
  1131. return promisedRequest({
  1132. url: MASTODON_STATUS_REBLOGGEDBY_URL(id),
  1133. method: 'GET',
  1134. credentials
  1135. }).then((users) => users.map(parseUser))
  1136. }
  1137. const fetchEmojiReactions = ({ id, credentials }) => {
  1138. return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id), credentials })
  1139. .then((reactions) => reactions.map(r => {
  1140. r.accounts = r.accounts.map(parseUser)
  1141. return r
  1142. }))
  1143. }
  1144. const reactWithEmoji = ({ id, emoji, credentials }) => {
  1145. return promisedRequest({
  1146. url: PLEROMA_EMOJI_REACT_URL(id, emoji),
  1147. method: 'PUT',
  1148. credentials
  1149. }).then(parseStatus)
  1150. }
  1151. const unreactWithEmoji = ({ id, emoji, credentials }) => {
  1152. return promisedRequest({
  1153. url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
  1154. method: 'DELETE',
  1155. credentials
  1156. }).then(parseStatus)
  1157. }
  1158. const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
  1159. return promisedRequest({
  1160. url: MASTODON_REPORT_USER_URL,
  1161. method: 'POST',
  1162. payload: {
  1163. account_id: userId,
  1164. status_ids: statusIds,
  1165. comment,
  1166. forward
  1167. },
  1168. credentials
  1169. })
  1170. }
  1171. const searchUsers = ({ credentials, query }) => {
  1172. return promisedRequest({
  1173. url: MASTODON_USER_SEARCH_URL,
  1174. params: {
  1175. q: query,
  1176. resolve: true
  1177. },
  1178. credentials
  1179. })
  1180. .then((data) => data.map(parseUser))
  1181. }
  1182. const search2 = ({ credentials, q, resolve, limit, offset, following, type }) => {
  1183. let url = MASTODON_SEARCH_2
  1184. const params = []
  1185. if (q) {
  1186. params.push(['q', encodeURIComponent(q)])
  1187. }
  1188. if (resolve) {
  1189. params.push(['resolve', resolve])
  1190. }
  1191. if (limit) {
  1192. params.push(['limit', limit])
  1193. }
  1194. if (offset) {
  1195. params.push(['offset', offset])
  1196. }
  1197. if (following) {
  1198. params.push(['following', true])
  1199. }
  1200. if (type) {
  1201. params.push(['following', type])
  1202. }
  1203. params.push(['with_relationships', true])
  1204. const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
  1205. url += `?${queryString}`
  1206. return fetch(url, { headers: authHeaders(credentials) })
  1207. .then((data) => {
  1208. if (data.ok) {
  1209. return data
  1210. }
  1211. throw new Error('Error fetching search result', data)
  1212. })
  1213. .then((data) => { return data.json() })
  1214. .then((data) => {
  1215. data.accounts = data.accounts.slice(0, limit).map(u => parseUser(u))
  1216. data.statuses = data.statuses.slice(0, limit).map(s => parseStatus(s))
  1217. return data
  1218. })
  1219. }
  1220. const fetchKnownDomains = ({ credentials }) => {
  1221. return promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials })
  1222. }
  1223. const fetchDomainMutes = ({ credentials }) => {
  1224. return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
  1225. }
  1226. const muteDomain = ({ domain, credentials }) => {
  1227. return promisedRequest({
  1228. url: MASTODON_DOMAIN_BLOCKS_URL,
  1229. method: 'POST',
  1230. payload: { domain },
  1231. credentials
  1232. })
  1233. }
  1234. const unmuteDomain = ({ domain, credentials }) => {
  1235. return promisedRequest({
  1236. url: MASTODON_DOMAIN_BLOCKS_URL,
  1237. method: 'DELETE',
  1238. payload: { domain },
  1239. credentials
  1240. })
  1241. }
  1242. const dismissNotification = ({ credentials, id }) => {
  1243. return promisedRequest({
  1244. url: MASTODON_DISMISS_NOTIFICATION_URL(id),
  1245. method: 'POST',
  1246. payload: { id },
  1247. credentials
  1248. })
  1249. }
  1250. const adminFetchAnnouncements = ({ credentials }) => {
  1251. return promisedRequest({ url: PLEROMA_ANNOUNCEMENTS_URL, credentials })
  1252. }
  1253. const fetchAnnouncements = ({ credentials }) => {
  1254. return promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials })
  1255. }
  1256. const dismissAnnouncement = ({ id, credentials }) => {
  1257. return promisedRequest({
  1258. url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id),
  1259. credentials,
  1260. method: 'POST'
  1261. })
  1262. }
  1263. const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => {
  1264. const payload = { content }
  1265. if (typeof startsAt !== 'undefined') {
  1266. payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null
  1267. }
  1268. if (typeof endsAt !== 'undefined') {
  1269. payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null
  1270. }
  1271. if (typeof allDay !== 'undefined') {
  1272. payload.all_day = allDay
  1273. }
  1274. return payload
  1275. }
  1276. const postAnnouncement = ({ credentials, content, startsAt, endsAt, allDay }) => {
  1277. return promisedRequest({
  1278. url: PLEROMA_POST_ANNOUNCEMENT_URL,
  1279. credentials,
  1280. method: 'POST',
  1281. payload: announcementToPayload({ content, startsAt, endsAt, allDay })
  1282. })
  1283. }
  1284. const editAnnouncement = ({ id, credentials, content, startsAt, endsAt, allDay }) => {
  1285. return promisedRequest({
  1286. url: PLEROMA_EDIT_ANNOUNCEMENT_URL(id),
  1287. credentials,
  1288. method: 'PATCH',
  1289. payload: announcementToPayload({ content, startsAt, endsAt, allDay })
  1290. })
  1291. }
  1292. const deleteAnnouncement = ({ id, credentials }) => {
  1293. return promisedRequest({
  1294. url: PLEROMA_DELETE_ANNOUNCEMENT_URL(id),
  1295. credentials,
  1296. method: 'DELETE'
  1297. })
  1298. }
  1299. export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
  1300. return Object.entries({
  1301. ...(credentials
  1302. ? { access_token: credentials }
  1303. : {}
  1304. ),
  1305. stream,
  1306. ...args
  1307. }).reduce((acc, [key, val]) => {
  1308. return acc + `${key}=${val}&`
  1309. }, MASTODON_STREAMING + '?')
  1310. }
  1311. const MASTODON_STREAMING_EVENTS = new Set([
  1312. 'update',
  1313. 'notification',
  1314. 'delete',
  1315. 'filters_changed',
  1316. 'status.update'
  1317. ])
  1318. const PLEROMA_STREAMING_EVENTS = new Set([
  1319. 'pleroma:chat_update'
  1320. ])
  1321. // A thin wrapper around WebSocket API that allows adding a pre-processor to it
  1322. // Uses EventTarget and a CustomEvent to proxy events
  1323. export const ProcessedWS = ({
  1324. url,
  1325. preprocessor = handleMastoWS,
  1326. id = 'Unknown'
  1327. }) => {
  1328. const eventTarget = new EventTarget()
  1329. const socket = new WebSocket(url)
  1330. if (!socket) throw new Error(`Failed to create socket ${id}`)
  1331. const proxy = (original, eventName, processor = a => a) => {
  1332. original.addEventListener(eventName, (eventData) => {
  1333. eventTarget.dispatchEvent(new CustomEvent(
  1334. eventName,
  1335. { detail: processor(eventData) }
  1336. ))
  1337. })
  1338. }
  1339. socket.addEventListener('open', (wsEvent) => {
  1340. console.debug(`[WS][${id}] Socket connected`, wsEvent)
  1341. })
  1342. socket.addEventListener('error', (wsEvent) => {
  1343. console.debug(`[WS][${id}] Socket errored`, wsEvent)
  1344. })
  1345. socket.addEventListener('close', (wsEvent) => {
  1346. console.debug(
  1347. `[WS][${id}] Socket disconnected with code ${wsEvent.code}`,
  1348. wsEvent
  1349. )
  1350. })
  1351. // Commented code reason: very spammy, uncomment to enable message debug logging
  1352. /*
  1353. socket.addEventListener('message', (wsEvent) => {
  1354. console.debug(
  1355. `[WS][${id}] Message received`,
  1356. wsEvent
  1357. )
  1358. })
  1359. /**/
  1360. proxy(socket, 'open')
  1361. proxy(socket, 'close')
  1362. proxy(socket, 'message', preprocessor)
  1363. proxy(socket, 'error')
  1364. // 1000 = Normal Closure
  1365. eventTarget.close = () => { socket.close(1000, 'Shutting down socket') }
  1366. eventTarget.getState = () => socket.readyState
  1367. return eventTarget
  1368. }
  1369. export const handleMastoWS = (wsEvent) => {
  1370. const { data } = wsEvent
  1371. if (!data) return
  1372. const parsedEvent = JSON.parse(data)
  1373. const { event, payload } = parsedEvent
  1374. if (MASTODON_STREAMING_EVENTS.has(event) || PLEROMA_STREAMING_EVENTS.has(event)) {
  1375. // MastoBE and PleromaBE both send payload for delete as a PLAIN string
  1376. if (event === 'delete') {
  1377. return { event, id: payload }
  1378. }
  1379. const data = payload ? JSON.parse(payload) : null
  1380. if (event === 'update') {
  1381. return { event, status: parseStatus(data) }
  1382. } else if (event === 'status.update') {
  1383. return { event, status: parseStatus(data) }
  1384. } else if (event === 'notification') {
  1385. return { event, notification: parseNotification(data) }
  1386. } else if (event === 'pleroma:chat_update') {
  1387. return { event, chatUpdate: parseChat(data) }
  1388. }
  1389. } else {
  1390. console.warn('Unknown event', wsEvent)
  1391. return null
  1392. }
  1393. }
  1394. export const WSConnectionStatus = Object.freeze({
  1395. JOINED: 1,
  1396. CLOSED: 2,
  1397. ERROR: 3,
  1398. DISABLED: 4,
  1399. STARTING: 5,
  1400. STARTING_INITIAL: 6
  1401. })
  1402. const chats = ({ credentials }) => {
  1403. return fetch(PLEROMA_CHATS_URL, { headers: authHeaders(credentials) })
  1404. .then((data) => data.json())
  1405. .then((data) => {
  1406. return { chats: data.map(parseChat).filter(c => c) }
  1407. })
  1408. }
  1409. const getOrCreateChat = ({ accountId, credentials }) => {
  1410. return promisedRequest({
  1411. url: PLEROMA_CHAT_URL(accountId),
  1412. method: 'POST',
  1413. credentials
  1414. })
  1415. }
  1416. const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
  1417. let url = PLEROMA_CHAT_MESSAGES_URL(id)
  1418. const args = [
  1419. maxId && `max_id=${maxId}`,
  1420. sinceId && `since_id=${sinceId}`,
  1421. limit && `limit=${limit}`
  1422. ].filter(_ => _).join('&')
  1423. url = url + (args ? '?' + args : '')
  1424. return promisedRequest({
  1425. url,
  1426. method: 'GET',
  1427. credentials
  1428. })
  1429. }
  1430. const sendChatMessage = ({ id, content, mediaId = null, idempotencyKey, credentials }) => {
  1431. const payload = {
  1432. content
  1433. }
  1434. if (mediaId) {
  1435. payload.media_id = mediaId
  1436. }
  1437. const headers = {}
  1438. if (idempotencyKey) {
  1439. headers['idempotency-key'] = idempotencyKey
  1440. }
  1441. return promisedRequest({
  1442. url: PLEROMA_CHAT_MESSAGES_URL(id),
  1443. method: 'POST',
  1444. payload,
  1445. credentials,
  1446. headers
  1447. })
  1448. }
  1449. const readChat = ({ id, lastReadId, credentials }) => {
  1450. return promisedRequest({
  1451. url: PLEROMA_CHAT_READ_URL(id),
  1452. method: 'POST',
  1453. payload: {
  1454. last_read_id: lastReadId
  1455. },
  1456. credentials
  1457. })
  1458. }
  1459. const deleteChatMessage = ({ chatId, messageId, credentials }) => {
  1460. return promisedRequest({
  1461. url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId),
  1462. method: 'DELETE',
  1463. credentials
  1464. })
  1465. }
  1466. const setReportState = ({ id, state, credentials }) => {
  1467. // TODO: Can't use promisedRequest because on OK this does not return json
  1468. // See https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1322
  1469. return fetch(PLEROMA_ADMIN_REPORTS, {
  1470. headers: {
  1471. ...authHeaders(credentials),
  1472. Accept: 'application/json',
  1473. 'Content-Type': 'application/json'
  1474. },
  1475. method: 'PATCH',
  1476. body: JSON.stringify({
  1477. reports: [{
  1478. id,
  1479. state
  1480. }]
  1481. })
  1482. })
  1483. .then(data => {
  1484. if (data.status >= 500) {
  1485. throw Error(data.statusText)
  1486. } else if (data.status >= 400) {
  1487. return data.json()
  1488. }
  1489. return data
  1490. })
  1491. .then(data => {
  1492. if (data.errors) {
  1493. throw Error(data.errors[0].message)
  1494. }
  1495. })
  1496. }
  1497. // ADMIN STUFF // EXPERIMENTAL
  1498. const fetchInstanceDBConfig = ({ credentials }) => {
  1499. return fetch(PLEROMA_ADMIN_CONFIG_URL, {
  1500. headers: authHeaders(credentials)
  1501. })
  1502. .then((response) => {
  1503. if (response.ok) {
  1504. return response.json()
  1505. } else {
  1506. return {
  1507. error: response
  1508. }
  1509. }
  1510. })
  1511. }
  1512. const fetchInstanceConfigDescriptions = ({ credentials }) => {
  1513. return fetch(PLEROMA_ADMIN_DESCRIPTIONS_URL, {
  1514. headers: authHeaders(credentials)
  1515. })
  1516. .then((response) => {
  1517. if (response.ok) {
  1518. return response.json()
  1519. } else {
  1520. return {
  1521. error: response
  1522. }
  1523. }
  1524. })
  1525. }
  1526. const fetchAvailableFrontends = ({ credentials }) => {
  1527. return fetch(PLEROMA_ADMIN_FRONTENDS_URL, {
  1528. headers: authHeaders(credentials)
  1529. })
  1530. .then((response) => {
  1531. if (response.ok) {
  1532. return response.json()
  1533. } else {
  1534. return {
  1535. error: response
  1536. }
  1537. }
  1538. })
  1539. }
  1540. const pushInstanceDBConfig = ({ credentials, payload }) => {
  1541. return fetch(PLEROMA_ADMIN_CONFIG_URL, {
  1542. headers: {
  1543. Accept: 'application/json',
  1544. 'Content-Type': 'application/json',
  1545. ...authHeaders(credentials)
  1546. },
  1547. method: 'POST',
  1548. body: JSON.stringify(payload)
  1549. })
  1550. .then((response) => {
  1551. if (response.ok) {
  1552. return response.json()
  1553. } else {
  1554. return {
  1555. error: response
  1556. }
  1557. }
  1558. })
  1559. }
  1560. const installFrontend = ({ credentials, payload }) => {
  1561. return fetch(PLEROMA_ADMIN_FRONTENDS_INSTALL_URL, {
  1562. headers: {
  1563. Accept: 'application/json',
  1564. 'Content-Type': 'application/json',
  1565. ...authHeaders(credentials)
  1566. },
  1567. method: 'POST',
  1568. body: JSON.stringify(payload)
  1569. })
  1570. .then((response) => {
  1571. if (response.ok) {
  1572. return response.json()
  1573. } else {
  1574. return {
  1575. error: response
  1576. }
  1577. }
  1578. })
  1579. }
  1580. const fetchScrobbles = ({ accountId, limit = 1 }) => {
  1581. let url = PLEROMA_SCROBBLES_URL(accountId)
  1582. const params = [['limit', limit]]
  1583. const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
  1584. url += `?${queryString}`
  1585. return fetch(url, {})
  1586. .then((response) => {
  1587. if (response.ok) {
  1588. return response.json()
  1589. } else {
  1590. return {
  1591. error: response
  1592. }
  1593. }
  1594. })
  1595. }
  1596. const deleteEmojiPack = ({ name }) => {
  1597. return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'DELETE' })
  1598. }
  1599. const reloadEmoji = () => {
  1600. return fetch(PLEROMA_EMOJI_RELOAD_URL, { method: 'POST' })
  1601. }
  1602. const importEmojiFromFS = () => {
  1603. return fetch(PLEROMA_EMOJI_IMPORT_FS_URL)
  1604. }
  1605. const createEmojiPack = ({ name }) => {
  1606. return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'POST' })
  1607. }
  1608. const listEmojiPacks = ({ page, pageSize }) => {
  1609. return fetch(PLEROMA_EMOJI_PACKS_URL(page, pageSize))
  1610. }
  1611. const listRemoteEmojiPacks = ({ instance, page, pageSize }) => {
  1612. if (!instance.startsWith('http')) {
  1613. instance = 'https://' + instance
  1614. }
  1615. return fetch(
  1616. PLEROMA_EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize),
  1617. {
  1618. headers: { 'Content-Type': 'application/json' }
  1619. }
  1620. )
  1621. }
  1622. const downloadRemoteEmojiPack = ({ instance, packName, as }) => {
  1623. return fetch(
  1624. PLEROMA_EMOJI_PACKS_DL_REMOTE_URL,
  1625. {
  1626. method: 'POST',
  1627. headers: { 'Content-Type': 'application/json' },
  1628. body: JSON.stringify({
  1629. url: instance, name: packName, as
  1630. })
  1631. }
  1632. )
  1633. }
  1634. const saveEmojiPackMetadata = ({ name, newData }) => {
  1635. return fetch(
  1636. PLEROMA_EMOJI_PACK_URL(name),
  1637. {
  1638. method: 'PATCH',
  1639. headers: { 'Content-Type': 'application/json' },
  1640. body: JSON.stringify({ metadata: newData })
  1641. }
  1642. )
  1643. }
  1644. const addNewEmojiFile = ({ packName, file, shortcode, filename }) => {
  1645. const data = new FormData()
  1646. if (filename.trim() !== '') { data.set('filename', filename) }
  1647. if (shortcode.trim() !== '') { data.set('shortcode', shortcode) }
  1648. data.set('file', file)
  1649. return fetch(
  1650. PLEROMA_EMOJI_UPDATE_FILE_URL(packName),
  1651. { method: 'POST', body: data }
  1652. )
  1653. }
  1654. const updateEmojiFile = ({ packName, shortcode, newShortcode, newFilename, force }) => {
  1655. return fetch(
  1656. PLEROMA_EMOJI_UPDATE_FILE_URL(packName),
  1657. {
  1658. method: 'PATCH',
  1659. headers: { 'Content-Type': 'application/json' },
  1660. body: JSON.stringify({ shortcode, new_shortcode: newShortcode, new_filename: newFilename, force })
  1661. }
  1662. )
  1663. }
  1664. const deleteEmojiFile = ({ packName, shortcode }) => {
  1665. return fetch(`${PLEROMA_EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, { method: 'DELETE' })
  1666. }
  1667. const apiService = {
  1668. verifyCredentials,
  1669. fetchTimeline,
  1670. fetchPinnedStatuses,
  1671. fetchConversation,
  1672. fetchStatus,
  1673. fetchStatusSource,
  1674. fetchStatusHistory,
  1675. fetchFriends,
  1676. exportFriends,
  1677. fetchFollowers,
  1678. followUser,
  1679. unfollowUser,
  1680. pinOwnStatus,
  1681. unpinOwnStatus,
  1682. muteConversation,
  1683. unmuteConversation,
  1684. blockUser,
  1685. unblockUser,
  1686. removeUserFromFollowers,
  1687. editUserNote,
  1688. fetchUser,
  1689. fetchUserByName,
  1690. fetchUserRelationship,
  1691. favorite,
  1692. unfavorite,
  1693. retweet,
  1694. unretweet,
  1695. bookmarkStatus,
  1696. unbookmarkStatus,
  1697. postStatus,
  1698. editStatus,
  1699. deleteStatus,
  1700. uploadMedia,
  1701. setMediaDescription,
  1702. fetchMutes,
  1703. muteUser,
  1704. unmuteUser,
  1705. subscribeUser,
  1706. unsubscribeUser,
  1707. fetchBlocks,
  1708. fetchOAuthTokens,
  1709. revokeOAuthToken,
  1710. tagUser,
  1711. untagUser,
  1712. deleteUser,
  1713. addRight,
  1714. deleteRight,
  1715. activateUser,
  1716. deactivateUser,
  1717. register,
  1718. getCaptcha,
  1719. updateProfileImages,
  1720. updateProfile,
  1721. importMutes,
  1722. importBlocks,
  1723. importFollows,
  1724. deleteAccount,
  1725. changeEmail,
  1726. moveAccount,
  1727. addAlias,
  1728. deleteAlias,
  1729. listAliases,
  1730. changePassword,
  1731. settingsMFA,
  1732. mfaDisableOTP,
  1733. generateMfaBackupCodes,
  1734. mfaSetupOTP,
  1735. mfaConfirmOTP,
  1736. addBackup,
  1737. listBackups,
  1738. fetchFollowRequests,
  1739. fetchLists,
  1740. createList,
  1741. getList,
  1742. updateList,
  1743. getListAccounts,
  1744. addAccountsToList,
  1745. removeAccountsFromList,
  1746. deleteList,
  1747. approveUser,
  1748. denyUser,
  1749. suggestions,
  1750. markNotificationsAsSeen,
  1751. dismissNotification,
  1752. vote,
  1753. fetchPoll,
  1754. fetchFavoritedByUsers,
  1755. fetchRebloggedByUsers,
  1756. fetchEmojiReactions,
  1757. reactWithEmoji,
  1758. unreactWithEmoji,
  1759. reportUser,
  1760. updateNotificationSettings,
  1761. search2,
  1762. searchUsers,
  1763. fetchKnownDomains,
  1764. fetchDomainMutes,
  1765. muteDomain,
  1766. unmuteDomain,
  1767. chats,
  1768. getOrCreateChat,
  1769. chatMessages,
  1770. sendChatMessage,
  1771. readChat,
  1772. deleteChatMessage,
  1773. setReportState,
  1774. fetchUserInLists,
  1775. fetchAnnouncements,
  1776. dismissAnnouncement,
  1777. postAnnouncement,
  1778. editAnnouncement,
  1779. deleteAnnouncement,
  1780. fetchScrobbles,
  1781. adminFetchAnnouncements,
  1782. fetchInstanceDBConfig,
  1783. fetchInstanceConfigDescriptions,
  1784. fetchAvailableFrontends,
  1785. pushInstanceDBConfig,
  1786. installFrontend,
  1787. importEmojiFromFS,
  1788. reloadEmoji,
  1789. listEmojiPacks,
  1790. createEmojiPack,
  1791. deleteEmojiPack,
  1792. saveEmojiPackMetadata,
  1793. addNewEmojiFile,
  1794. updateEmojiFile,
  1795. deleteEmojiFile,
  1796. listRemoteEmojiPacks,
  1797. downloadRemoteEmojiPack
  1798. }
  1799. export default apiService