Revamp Music API, add E2E, update Spectrum and User

Music model: new method structure, updated API endpoints (tracks,
releases, lyrics, library).
E2E (wip): new model for encryption key pair management.
Spectrum: updated API to support new
spectrum api.
User: added public key management.
Session, Auth, Radio: minor API enhancements.
This commit is contained in:
SrGooglo 2025-05-10 02:35:38 +00:00
parent 511a81e313
commit e035e6bde4
23 changed files with 453 additions and 533 deletions

View File

@ -42,7 +42,6 @@ export function createClient({
mainOrigin: origin,
baseRequest: null,
ws: null,
rest: null,
version: pkg.version,
addons: new AddonsManager(),
})

View File

@ -35,7 +35,7 @@ export default class AuthModel {
SessionModel.refreshToken = response.data.refreshToken
if (typeof callback === "function") {
await callback()
await callback(response.data)
}
__comty_shared_state.eventBus.emit("auth:login_success")

30
src/models/e2e/index.js Normal file
View File

@ -0,0 +1,30 @@
import SessionModel from "../session"
import request from "../../request"
export default class E2EModel {
static async getKeyPair() {
const response = await request({
method: "GET",
url: "/users/self/keypair",
})
return response.data
}
// WARNING: updating keypair makes all decryption fail
static async updateKeyPair(str, { imSure = false } = {}) {
if (imSure !== true) {
throw new Error(
"Missing confirmation to update the keypair. Use `imSure = true` to proceed.",
)
}
const response = await request({
method: "POST",
url: "/users/self/keypair",
data: { str: str },
})
return response.data
}
}

View File

@ -1,11 +0,0 @@
import request from "../../../request"
export default async () => {
const response = await request({
method: "GET",
url: "/music/playlists/featured",
})
// @ts-ignore
return response.data
}

View File

@ -0,0 +1,25 @@
import request from "../../../request"
export default async (type, item_id) => {
if (!type) {
throw new Error("type is required")
}
if (!item_id) {
throw new Error("item_id is required")
}
type = type.toLowerCase()
const response = await request({
method: "GET",
url: `/music/my/library/favorite`,
params: {
kind: type,
item_id: item_id,
},
})
// @ts-ignore
return response.data
}

View File

@ -1,33 +0,0 @@
import request from "../../../request"
const typeToNamespace = {
track: "tracks",
//playlist: "playlists",
//release: "releases",
}
export default async (type, track_id) => {
if (!type) {
throw new Error("type is required")
}
if (!track_id) {
throw new Error("track_id is required")
}
type = type.toLowerCase()
type = typeToNamespace[type]
if (!type) {
throw new Error(`Unsupported type: ${type}`)
}
const response = await request({
method: "GET",
url: `/music/${type}/${track_id}/is_favourite`,
})
// @ts-ignore
return response.data
}

View File

@ -2,24 +2,24 @@ import request from "../../../request"
import processAddons from "../../../helpers/processWithAddons"
import standartListMerge from "../../../utils/standartListMerge"
export default async ({ limit = 100, offset = 0, order = "desc" }) => {
const addons =
__comty_shared_state.addons.getByOperation("getFavoriteFolder")
export default async ({ limit = 100, offset = 0, order = "desc", kind }) => {
const addons = __comty_shared_state.addons.getByOperation("getMyLibrary")
const dividedLimit = limit / (addons.length + 1)
const { data } = await request({
method: "GET",
url: "/music/my/folder",
url: "/music/my/library",
params: {
limit: dividedLimit,
offset: offset,
order: order,
kind: kind,
},
})
let results = await processAddons({
operation: "getFavoriteFolder",
operation: "getMyLibrary",
initialData: data,
fnArguments: [{ limit: dividedLimit, offset: offset, order: order }],
normalizeAddonResult: ({ currentData, addonResult }) => {
@ -27,16 +27,18 @@ export default async ({ limit = 100, offset = 0, order = "desc" }) => {
},
})
// sort by liked_at
results.tracks.items.sort((a, b) => {
if (a.liked_at > b.liked_at) {
return -1
}
if (a.liked_at < b.liked_at) {
return 1
}
return 0
})
// sort tracks by liked_at
if (results.tracks) {
results.tracks.items.sort((a, b) => {
if (a.liked_at > b.liked_at) {
return -1
}
if (a.liked_at < b.liked_at) {
return 1
}
return 0
})
}
return results
}

View File

@ -1,26 +1,22 @@
import request from "../../../request"
type Arguments = {
limit: Number
offset: Number
keywords: String
limit: Number
offset: Number
keywords: String
}
export default async ({
limit,
offset,
keywords,
}: Arguments) => {
const response = await request({
method: "GET",
url: "/music/releases/self",
params: {
limit: limit,
offset: offset,
keywords: keywords,
}
})
export default async ({ limit, offset, keywords }: Arguments) => {
const response = await request({
method: "GET",
url: "/music/my/releases",
params: {
limit: limit,
offset: offset,
keywords: keywords,
},
})
// @ts-ignore
return response.data
}
// @ts-ignore
return response.data
}

View File

@ -1,11 +0,0 @@
import request from "../../../request"
export default async (id: String) => {
const response = await request({
method: "GET",
url: `/music/playlists/${id}/data`,
})
// @ts-ignore
return response.data
}

View File

@ -1,11 +0,0 @@
import request from "../../../request"
export default async (id: String) => {
const response = await request({
method: "GET",
url: `/music/playlists/${id}/items`,
})
// @ts-ignore
return response.data
}

View File

@ -1,29 +0,0 @@
import request from "../../../request"
type Arguments = {
keywords: String
user_id: String
limit: Number
offset: Number
}
export default async ({
keywords,
user_id,
limit,
offset,
}: Arguments) => {
const response = await request({
method: "GET",
url: "/music/playlists",
params: {
keywords: keywords,
user_id: user_id,
limit: limit,
offset: offset,
}
})
// @ts-ignore
return response.data
}

View File

@ -1,29 +1,22 @@
import request from "../../../request"
type Arguments = {
keywords: String
user_id: String
limit: Number
offset: Number
user_id: String
limit: Number
page: Number
}
export default async ({
keywords,
user_id,
limit,
offset,
}: Arguments) => {
const response = await request({
method: "GET",
url: "/music/releases",
params: {
keywords: keywords,
user_id: user_id,
limit: limit,
offset: offset,
}
})
export default async ({ user_id, limit, page }: Arguments) => {
const response = await request({
method: "GET",
url: "/music/releases",
params: {
user_id: user_id,
limit: limit,
page: page,
},
})
// @ts-ignore
return response.data
}
// @ts-ignore
return response.data
}

View File

@ -1,31 +1,31 @@
import request from "../../../request"
type RequestOptions = {
preferTranslation?: Boolean
preferTranslation?: Boolean
}
type RequestParams = {
translate_lang?: String
translate_lang?: String
}
export default async (
id: String,
options: RequestOptions = {
preferTranslation: false,
}
id: String,
options: RequestOptions = {
preferTranslation: false,
},
) => {
const requestParams: RequestParams = Object()
const requestParams: RequestParams = Object()
if (options.preferTranslation) {
requestParams.translate_lang = app.cores.settings.get("app:language")
}
if (options.preferTranslation) {
requestParams.translate_lang = app.cores.settings.get("app:language")
}
const response = await request({
method: "GET",
url: `/music/lyrics/${id}`,
params: requestParams
})
const response = await request({
method: "GET",
url: `/music/tracks/${id}/lyrics`,
params: requestParams,
})
// @ts-ignore
return response.data
}
// @ts-ignore
return response.data
}

View File

@ -1,29 +1,22 @@
import request from "../../../request"
type Arguments = {
keywords: String
user_id: String
limit: Number
offset: Number
user_id: String
limit: Number
page: Number
}
export default async ({
keywords,
user_id,
limit,
offset,
}: Arguments) => {
const response = await request({
method: "GET",
url: "/music/tracks",
params: {
keywords: keywords,
user_id: user_id,
limit: limit,
offset: offset,
}
})
export default async ({ user_id, limit, page }: Arguments) => {
const response = await request({
method: "GET",
url: "/music/tracks",
params: {
user_id: user_id,
limit: limit,
page: page,
},
})
// @ts-ignore
return response.data
}
// @ts-ignore
return response.data
}

View File

@ -2,150 +2,37 @@ import Getters from "./getters"
import Setters from "./setters"
export default class MusicModel {
static Getters = Getters
static Setters = Setters
static Getters = Getters
static Setters = Setters
/**
* Performs a search based on the provided keywords, with optional parameters for limiting the number of results and pagination.
*
* @param {string} keywords - The keywords to search for.
* @param {object} options - An optional object containing additional parameters.
* @param {number} options.limit - The maximum number of results to return. Defaults to 5.
* @param {number} options.offset - The offset to start returning results from. Defaults to 0.
* @param {boolean} options.useTidal - Whether to use Tidal for the search. Defaults to false.
* @return {Promise<Object>} The search results.
*/
static search = Getters.search
// track related methods
static getMyTracks = null
static getAllTracks = Getters.tracks
static getTrackData = Getters.trackData
static putTrack = Setters.putTrack
static deleteTrack = null
/**
* Retrieves playlist items based on the provided parameters.
*
* @param {Object} options - The options object.
* @param {string} options.playlist_id - The ID of the playlist.
* @param {string} options.service - The service from which to retrieve the playlist items.
* @param {number} options.limit - The maximum number of items to retrieve.
* @param {number} options.offset - The number of items to skip before retrieving.
* @return {Promise<Object>} Playlist items data.
*/
static getPlaylistItems = Getters.PlaylistItems
// lyrics related methods
static getTrackLyrics = Getters.trackLyrics
static putTrackLyrics = Setters.putTrackLyrics
/**
* Retrieves playlist data based on the provided parameters.
*
* @param {Object} options - The options object.
* @param {string} options.playlist_id - The ID of the playlist.
* @param {string} options.service - The service to use.
* @param {number} options.limit - The maximum number of items to retrieve.
* @param {number} options.offset - The offset for pagination.
* @return {Promise<Object>} Playlist data.
*/
static getPlaylistData = Getters.PlaylistData
// release related methods
static getMyReleases = Getters.myReleases
static getAllReleases = Getters.releases
static getReleaseData = Getters.releaseData
static putRelease = Setters.putRelease
static deleteRelease = Setters.deleteRelease
/**
* Retrieves releases based on the provided parameters.
* If user_id is not provided, it will retrieve self authenticated user releases.
*
* @param {object} options - The options for retrieving releases.
* @param {string} options.user_id - The ID of the user.
* @param {string[]} options.keywords - The keywords to filter releases by.
* @param {number} options.limit - The maximum number of releases to retrieve.
* @param {number} options.offset - The offset for paginated results.
* @return {Promise<Object>} - A promise that resolves to the retrieved releases.
*/
static getReleases = Getters.releases
// library related methods
static getMyLibrary = Getters.library
static toggleItemFavorite = Setters.toggleItemFavorite
static isItemFavorited = Getters.isItemFavorited
/**
* Retrieves self releases.
*
* @param {object} options - The options for retrieving my releases.
* @param {number} options.limit - The maximum number of releases to retrieve.
* @param {number} options.offset - The offset for paginated results.
* @return {Promise<Object>} - A promise that resolves to the retrieved releases.
*/
static getMyReleases = Getters.myReleases
// other methods
static getRecentyPlayed = Getters.recentlyPlayed
static search = Getters.search
/**
* Retrieves release data by ID.
*
* @param {number} id - The ID of the release.
* @return {Promise<Object>} The release data.
*/
static getReleaseData = Getters.releaseData
/**
* Retrieves track data for a given ID.
*
* @param {string} id - The ID of the track or multiple IDs separated by commas.
* @return {Promise<Object>} The track data.
*/
static getTrackData = Getters.trackData
/**
* Retrieves the official featured playlists.
*
* @return {Promise<Object>} The data containing the featured playlists.
*/
static getFeaturedPlaylists = Getters.featuredPlaylists
/**
* Retrieves track lyrics for a given ID.
*
* @param {string} id - The ID of the track.
* @return {Promise<Object>} The track lyrics.
*/
static getTrackLyrics = Getters.trackLyrics
static putTrackLyrics = Setters.putTrackLyrics
/**
* Create or modify a track.
*
* @param {object} TrackManifest - The track manifest.
* @return {Promise<Object>} The result track data.
*/
static putTrack = Setters.putTrack
/**
* Create or modify a release.
*
* @param {object} ReleaseManifest - The release manifest.
* @return {Promise<Object>} The result release data.
*/
static putRelease = Setters.putRelease
/**
* Deletes a release by its ID.
*
* @param {string} id - The ID of the release to delete.
* @return {Promise<Object>} - A Promise that resolves to the data returned by the API.
*/
static deleteRelease = Setters.deleteRelease
/**
* Retrieves the favourite tracks of the current user.
*
* @return {Promise<Object>} The favorite tracks data.
*/
static getFavouriteTracks = null
/**
* Retrieves the favourite tracks/playlists/releases of the current user.
*
* @return {Promise<Object>} The favorite playlists data.
*/
static getFavouriteFolder = Getters.favouriteFolder
/**
* Toggles the favourite status of a track, playlist or folder.
*
* @param {string} track_id - The ID of the track to toggle the favorite status.
* @throws {Error} If the track_id is not provided.
* @return {Promise<Object>} The response data after toggling the favorite status.
*/
static toggleItemFavourite = Setters.toggleItemFavourite
static isItemFavourited = Getters.isItemFavourited
static getRecentyPlayed = Getters.recentlyPlayed
}
// aliases
static toggleItemFavourite = MusicModel.toggleItemFavorite
static isItemFavourited = MusicModel.isItemFavorited
}

View File

@ -1,12 +1,12 @@
import request from "../../../request"
export default async (track_id, data) => {
const response = await request({
method: "put",
url: `/music/lyrics/${track_id}`,
data: data,
})
const response = await request({
method: "put",
url: `/music/tracks/${track_id}/lyrics`,
data: data,
})
// @ts-ignore
return response.data
}
// @ts-ignore
return response.data
}

View File

@ -0,0 +1,26 @@
import request from "../../../request"
export default async (type, item_id, to) => {
if (!type) {
throw new Error("type is required")
}
if (!item_id) {
throw new Error("item_id is required")
}
type = type.toLowerCase()
const response = await request({
method: "PUT",
url: `/music/my/library/favorite`,
data: {
item_id: item_id,
kind: type,
to: to,
},
})
// @ts-ignore
return response.data
}

View File

@ -1,36 +0,0 @@
import request from "../../../request"
const typeToNamespace = {
track: "tracks",
//playlist: "playlists",
//release: "releases",
}
export default async (type, track_id, to) => {
if (!type) {
throw new Error("type is required")
}
if (!track_id) {
throw new Error("track_id is required")
}
type = type.toLowerCase()
type = typeToNamespace[type]
if (!type) {
throw new Error(`Unsupported type: ${type}`)
}
const response = await request({
method: "post",
url: `/music/${type}/${track_id}/favourite`,
data: {
to: to,
}
})
// @ts-ignore
return response.data
}

View File

@ -0,0 +1,10 @@
import request from "../../../request"
export default async () => {
const { data } = await request({
method: "GET",
url: "/music/radio/trendings",
})
return data
}

View File

@ -1,5 +1,7 @@
import getRadioList from "./getters/list"
import getTrendings from "./getters/trendings"
export default class Radio {
static getRadioList = getRadioList
static getTrendings = getTrendings
}

View File

@ -3,163 +3,168 @@ import request from "../../request"
import Storage from "../../helpers/withStorage"
export default class Session {
static storageTokenKey = "token"
static storageRefreshTokenKey = "refreshToken"
static storageTokenKey = "token"
static storageRefreshTokenKey = "refreshToken"
/**
* Retrieves the token from the storage engine.
*
* @return {type} description of return value
*/
static get token() {
return Storage.engine.get(this.storageTokenKey)
}
/**
* Retrieves the token from the storage engine.
*
* @return {type} description of return value
*/
static get token() {
return Storage.engine.get(this.storageTokenKey)
}
/**
* Sets the token in the storage engine.
*
* @param {string} token - The token to be set.
* @return {Promise<void>} A promise that resolves when the token is successfully set.
*/
static set token(token) {
return Storage.engine.set(this.storageTokenKey, token)
}
/**
* Sets the token in the storage engine.
*
* @param {string} token - The token to be set.
* @return {Promise<void>} A promise that resolves when the token is successfully set.
*/
static set token(token) {
return Storage.engine.set(this.storageTokenKey, token)
}
/**
* Retrieves the refresh token from the storage engine.
*
* @return {string} The refresh token stored in the storage engine.
*/
static get refreshToken() {
return Storage.engine.get(this.storageRefreshTokenKey)
}
/**
* Retrieves the refresh token from the storage engine.
*
* @return {string} The refresh token stored in the storage engine.
*/
static get refreshToken() {
return Storage.engine.get(this.storageRefreshTokenKey)
}
/**
* Sets the refresh token in the storage engine.
*
* @param {string} token - The refresh token to be set.
* @return {Promise<void>} A promise that resolves when the refresh token is successfully set.
*/
static set refreshToken(token) {
return Storage.engine.set(this.storageRefreshTokenKey, token)
}
/**
* Sets the refresh token in the storage engine.
*
* @param {string} token - The refresh token to be set.
* @return {Promise<void>} A promise that resolves when the refresh token is successfully set.
*/
static set refreshToken(token) {
return Storage.engine.set(this.storageRefreshTokenKey, token)
}
/**
* Retrieves the roles from the decoded token object.
*
* @return {Array<string>|undefined} The roles if they exist, otherwise undefined.
*/
static get roles() {
return this.getDecodedToken()?.roles
}
/**
* Retrieves the roles from the decoded token object.
*
* @return {Array<string>|undefined} The roles if they exist, otherwise undefined.
*/
static get roles() {
return this.getDecodedToken()?.roles
}
/**
* Retrieves the user ID from the decoded token object.
*
* @return {string|undefined} The user ID if it exists, otherwise undefined.
*/
static get user_id() {
return this.getDecodedToken()?.user_id
}
/**
* Retrieves the user ID from the decoded token object.
*
* @return {string|undefined} The user ID if it exists, otherwise undefined.
*/
static get user_id() {
return this.getDecodedToken()?.user_id
}
/**
* Retrieves the session UUID from the decoded token object.
*
* @return {string} The session UUID if it exists, otherwise undefined.
*/
static get session_uuid() {
return this.getDecodedToken()?.session_uuid
}
/**
* Retrieves the session UUID from the decoded token object.
*
* @return {string} The session UUID if it exists, otherwise undefined.
*/
static get session_uuid() {
return this.getDecodedToken()?.session_uuid
}
/**
* Retrieves the decoded token from the session storage.
*
* @return {Object|null} The decoded token object if it exists, otherwise null.
*/
static getDecodedToken() {
const token = this.token
/**
* Retrieves the decoded token from the session storage.
*
* @return {Object|null} The decoded token object if it exists, otherwise null.
*/
static getDecodedToken() {
const token = this.token
return token && jwtDecode(token)
}
return token && jwtDecode(token)
}
/**
* Removes the token from the storage engine.
*
* @return {Promise<void>} A promise that resolves when the token is successfully removed.
*/
static removeToken() {
return Storage.engine.remove(Session.storageTokenKey)
}
/**
* Removes the token from the storage engine.
*
* @return {Promise<void>} A promise that resolves when the token is successfully removed.
*/
static removeToken() {
return Storage.engine.remove(Session.storageTokenKey)
}
/**
* Retrieves all sessions from the server.
*
* @return {Promise<Object>} The data of all sessions.
*/
static async getAllSessions() {
const response = await request({
method: "get",
url: "/sessions/all"
})
/**
* Retrieves all sessions from the server.
*
* @return {Promise<Object>} The data of all sessions.
*/
static async getAllSessions() {
const response = await request({
method: "get",
url: "/sessions/all",
})
return response.data
}
return response.data
}
/**
* Retrieves the current session from the server.
*
* @return {Promise<Object>} The data of the current session.
*/
static async getCurrentSession() {
const response = await request({
method: "get",
url: "/sessions/current"
})
/**
* Retrieves the current session from the server.
*
* @return {Promise<Object>} The data of the current session.
*/
static async getCurrentSession() {
const response = await request({
method: "get",
url: "/sessions/current",
})
return response.data
}
return response.data
}
/**
* Destroys the current session by deleting it from the server.
*
* @return {Promise<Object>} The response data from the server after deleting the session.
*/
static async destroyCurrentSession() {
const token = await Session.token
const session = await Session.getDecodedToken()
/**
* Destroys the current session by deleting it from the server.
*
* @return {Promise<Object>} The response data from the server after deleting the session.
*/
static async destroyCurrentSession() {
const token = await Session.token
const session = await Session.getDecodedToken()
if (!session || !token) {
return false
}
if (!session || !token) {
return false
}
const response = await request({
method: "delete",
url: "/sessions/current"
}).catch((error) => {
console.error(error)
const response = await request({
method: "delete",
url: "/sessions/current",
}).catch((error) => {
console.error(error)
return false
})
return false
})
Session.removeToken()
Session.removeToken()
__comty_shared_state.eventBus.emit("session.destroyed")
__comty_shared_state.eventBus.emit("session.destroyed")
return response.data
}
return response.data
}
static async destroyAllSessions() {
throw new Error("Not implemented")
}
static async destroyAll() {
const response = await request({
method: "delete",
url: "/sessions/all",
})
/**
* Retrieves the validity of the current token.
*
* @return {boolean} The validity status of the current token.
*/
static async isCurrentTokenValid() {
const health = await Session.getTokenValidation()
return response.data
}
return health.valid
}
}
/**
* Retrieves the validity of the current token.
*
* @return {boolean} The validity status of the current token.
*/
static async isCurrentTokenValid() {
const health = await Session.getTokenValidation()
return health.valid
}
}

View File

@ -1,8 +1,8 @@
import axios from "axios"
import { RTEngineClient } from "linebridge-client/src"
import SessionModel from "../session"
import UserModel from "../user"
import { RTEngineClient } from "linebridge-client/src"
//import { RTEngineClient } from "../../../../linebridge/client/src"
async function injectUserDataOnList(list) {
if (!Array.isArray(list)) {
@ -73,7 +73,7 @@ export default class Streaming {
const { data } = await Streaming.base({
method: "get",
url: `/streaming/${stream_id}`,
url: `/stream/${stream_id}/data`,
})
return data
@ -81,37 +81,51 @@ export default class Streaming {
static async getOwnProfiles() {
const { data } = await Streaming.base({
method: "get",
method: "GET",
url: "/streaming/profiles/self",
})
return data
}
static async getProfile({ profile_id }) {
static async getProfile(profile_id) {
if (!profile_id) {
return null
}
const { data } = await Streaming.base({
method: "get",
method: "GET",
url: `/streaming/profiles/${profile_id}`,
})
return data
}
static async createOrUpdateProfile(update) {
static async createProfile(payload) {
const { data } = await Streaming.base({
method: "put",
url: `/streaming/profiles/self`,
method: "POST",
url: "/streaming/profiles/new",
data: payload,
})
return data
}
static async updateProfile(profile_id, update) {
if (!profile_id) {
return null
}
const { data } = await Streaming.base({
method: "PUT",
url: `/streaming/profiles/${profile_id}`,
data: update,
})
return data
}
static async deleteProfile({ profile_id }) {
static async deleteProfile(profile_id) {
if (!profile_id) {
return null
}
@ -124,6 +138,36 @@ export default class Streaming {
return data
}
static async addRestreamToProfile(profileId, restreamData) {
if (!profileId) {
console.error("profileId is required to add a restream")
return null
}
const { data } = await Streaming.base({
method: "put",
url: `/streaming/profiles/${profileId}/restreams`,
data: restreamData,
})
return data
}
static async deleteRestreamFromProfile(profileId, restreamIndexData) {
if (!profileId) {
console.error("profileId is required to delete a restream")
return null
}
const { data } = await Streaming.base({
method: "delete",
url: `/streaming/profiles/${profileId}/restreams`,
data: restreamIndexData,
})
return data
}
static async list({ limit, offset } = {}) {
let { data } = await Streaming.base({
method: "get",
@ -145,11 +189,7 @@ export default class Streaming {
return null
}
const client = new RTEngineClient({
...params,
url: Streaming.apiHostname,
token: SessionModel.token,
})
const client = Streaming.createWebsocket(params)
client._destroy = client.destroy
@ -171,4 +211,14 @@ export default class Streaming {
return client
}
static createWebsocket(params = {}) {
const client = new RTEngineClient({
...params,
url: Streaming.apiHostname,
token: SessionModel.token,
})
return client
}
}

View File

@ -14,7 +14,15 @@ export default class User {
let { username, user_id, basic = false } = payload
if (!username && !user_id) {
user_id = SessionModel.user_id
const response = await request({
method: "GET",
url: `/users/self`,
params: {
basic,
},
})
return response.data
}
if (username && !user_id) {
@ -132,4 +140,29 @@ export default class User {
return data
}
static async getPublicKey(user_id) {
if (!user_id) {
user_id = SessionModel.user_id
}
const { data } = await request({
method: "GET",
url: `/users/${user_id}/public-key`,
})
return data
}
static async updatePublicKey(public_key) {
const { data } = await request({
method: "PUT",
url: `/users/self/public-key`,
data: {
public_key: public_key,
},
})
return data
}
}