/** * @fileoverview DevRant API Client for Rantii * @author retoor * @description HTTP client for communicating with the DevRant API * @keywords api, client, devrant, http, fetch */ const API_BASE_URL = window.location.hostname === 'localhost' ? '/api/' : 'https://dr.molodetz.nl/api/'; const APP_ID = 3; class ApiClient { constructor() { this.tokenId = null; this.tokenKey = null; this.userId = null; } setAuth(tokenId, tokenKey, userId) { this.tokenId = tokenId; this.tokenKey = tokenKey; this.userId = userId; } clearAuth() { this.tokenId = null; this.tokenKey = null; this.userId = null; } isAuthenticated() { return this.tokenId !== null && this.tokenKey !== null; } buildParams(params = {}) { const result = { app: APP_ID, ...params }; if (this.isAuthenticated()) { result.token_id = this.tokenId; result.token_key = this.tokenKey; result.user_id = this.userId; } return result; } buildUrl(endpoint) { const path = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint; return API_BASE_URL + path; } async request(method, endpoint, params = {}, timeout = 30000) { const url = this.buildUrl(endpoint); const allParams = this.buildParams(params); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const config = { method, headers: {}, signal: controller.signal }; try { let fullUrl = url; if (method === 'GET' || method === 'DELETE') { const queryString = new URLSearchParams(allParams).toString(); fullUrl = queryString ? `${url}?${queryString}` : url; } else { config.headers['Content-Type'] = 'application/x-www-form-urlencoded'; config.body = new URLSearchParams(allParams).toString(); } const response = await fetch(fullUrl, config); clearTimeout(timeoutId); if (!response.ok && response.status >= 500) { return { success: false, error: `Server error: ${response.status}` }; } const data = await response.json(); return data; } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { return { success: false, error: 'Request timed out' }; } return { success: false, error: error.message }; } } async get(endpoint, params = {}) { return this.request('GET', endpoint, params); } async post(endpoint, params = {}) { return this.request('POST', endpoint, params); } async delete(endpoint, params = {}) { return this.request('DELETE', endpoint, params); } async login(username, password) { const response = await this.post('/users/auth-token', { username, password }); if (response.success && response.auth_token) { this.setAuth( response.auth_token.id, response.auth_token.key, response.auth_token.user_id ); return { success: true, authToken: response.auth_token }; } return { success: false, error: response.error || 'Login failed' }; } async getRants(sort = 'recent', limit = 20, skip = 0) { const response = await this.get('/devrant/rants', { sort, limit, skip }); if (response.success) { return { success: true, rants: response.rants || [] }; } return { success: false, rants: [], error: response.error }; } async getRant(rantId) { const response = await this.get(`/devrant/rants/${rantId}`); if (response.success) { return { success: true, rant: response.rant, comments: response.comments || [] }; } return { success: false, error: response.error }; } async voteRant(rantId, vote, reason = null) { const params = { vote }; if (reason !== null) { params.reason = reason; } const response = await this.post(`/devrant/rants/${rantId}/vote`, params); return { success: response.success, rant: response.rant }; } async postComment(rantId, comment) { const response = await this.post(`/devrant/rants/${rantId}/comments`, { comment, plat: 2 }); return { success: response.success, comment: response.comment }; } async updateComment(commentId, comment) { const response = await this.post(`/comments/${commentId}`, { comment }); return { success: response.success }; } async deleteComment(commentId) { const response = await this.delete(`/comments/${commentId}`); return { success: response.success }; } async voteComment(commentId, vote, reason = null) { const params = { vote }; if (reason !== null) { params.reason = reason; } const response = await this.post(`/comments/${commentId}/vote`, params); return { success: response.success, comment: response.comment }; } async getComment(commentId) { const response = await this.get(`/comments/${commentId}`); if (response.success) { return { success: true, comment: response.comment }; } return { success: false, error: response.error }; } async getUserId(username) { const response = await this.get('/get-user-id', { username }); if (response.success) { return { success: true, userId: response.user_id }; } return { success: false, error: response.error }; } async getProfile(userId) { const response = await this.get(`/users/${userId}`); if (response.success) { return { success: true, profile: response.profile }; } return { success: false, error: response.error }; } async getProfileByUsername(username) { const userIdResponse = await this.getUserId(username); if (!userIdResponse.success) { return { success: false, error: userIdResponse.error }; } return this.getProfile(userIdResponse.userId); } async search(term) { const response = await this.get('/devrant/search', { term }); if (response.success) { return { success: true, rants: response.results || [] }; } return { success: false, rants: [], error: response.error }; } async getNotifications() { const response = await this.get('/users/me/notif-feed', { ext_prof: 1 }); if (response.success) { const items = response.data?.items || []; const users = response.data?.username_map || {}; const notifications = items.map(item => ({ ...item, username: users[item.uid] || item.username || 'Someone' })); return { success: true, notifications, unread: response.data?.unread || { total: 0 } }; } return { success: false, notifications: [], error: response.error }; } async clearNotifications() { const response = await this.delete('/users/me/notif-feed'); return { success: response.success }; } async postRant(text, tags = '', type = 1) { const response = await this.post('/devrant/rants', { rant: text, tags, type, plat: 2 }); return { success: response.success, rantId: response.rant_id }; } async deleteRant(rantId) { const response = await this.delete(`/devrant/rants/${rantId}`); return { success: response.success }; } async getWeeklyRants(sort = 'recent', limit = 20, skip = 0) { const response = await this.get('/devrant/weekly-rants', { sort, limit, skip }); if (response.success) { return { success: true, rants: response.rants || [] }; } return { success: false, rants: [], error: response.error }; } async getCollabs(sort = 'recent', limit = 20, skip = 0) { const response = await this.get('/devrant/collabs', { sort, limit, skip }); if (response.success) { return { success: true, rants: response.rants || [] }; } return { success: false, rants: [], error: response.error }; } async getStories(sort = 'recent', limit = 20, skip = 0) { const response = await this.get('/devrant/story-rants', { sort, limit, skip }); if (response.success) { return { success: true, rants: response.rants || [] }; } return { success: false, rants: [], error: response.error }; } } export { ApiClient, API_BASE_URL, APP_ID };