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 (55313B)


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