mirror of
https://github.com/woodchen-ink/obsidian-publish-to-discourse.git
synced 2025-07-18 05:42:05 +08:00
432 lines
15 KiB
TypeScript
432 lines
15 KiB
TypeScript
import { App, requestUrl, TFile } from 'obsidian';
|
|
import { DiscourseSyncSettings } from './config';
|
|
import { NotifyUser } from './notification';
|
|
import { t } from './i18n';
|
|
|
|
// 生成随机边界字符串
|
|
const genBoundary = (): string => {
|
|
return 'ObsidianBoundary' + Math.random().toString(16).substring(2);
|
|
};
|
|
|
|
export class DiscourseAPI {
|
|
constructor(
|
|
private app: App,
|
|
private settings: DiscourseSyncSettings
|
|
) {}
|
|
|
|
// 上传图片到Discourse
|
|
async uploadImage(file: TFile): Promise<string | null> {
|
|
try {
|
|
const imgfile = await this.app.vault.readBinary(file);
|
|
const boundary = genBoundary();
|
|
const sBoundary = '--' + boundary + '\r\n';
|
|
const imgForm = `${sBoundary}Content-Disposition: form-data; name="file"; filename="${file.name}"\r\nContent-Type: image/${file.extension}\r\n\r\n`;
|
|
|
|
let body = '';
|
|
body += `\r\n${sBoundary}Content-Disposition: form-data; name="type"\r\n\r\ncomposer\r\n`;
|
|
body += `${sBoundary}Content-Disposition: form-data; name="synchronous"\r\n\r\ntrue\r\n`;
|
|
|
|
const eBoundary = '\r\n--' + boundary + '--\r\n';
|
|
const imgFormArray = new TextEncoder().encode(imgForm);
|
|
const bodyArray = new TextEncoder().encode(body);
|
|
const endBoundaryArray = new TextEncoder().encode(eBoundary);
|
|
|
|
const formDataArray = new Uint8Array(imgFormArray.length + imgfile.byteLength + bodyArray.length + endBoundaryArray.length);
|
|
formDataArray.set(imgFormArray, 0);
|
|
formDataArray.set(new Uint8Array(imgfile), imgFormArray.length);
|
|
formDataArray.set(bodyArray, imgFormArray.length + imgfile.byteLength);
|
|
formDataArray.set(endBoundaryArray, imgFormArray.length + bodyArray.length + imgfile.byteLength);
|
|
|
|
const url = `${this.settings.baseUrl}/uploads.json`;
|
|
const headers = {
|
|
"Api-Key": this.settings.apiKey,
|
|
"Api-Username": this.settings.disUser,
|
|
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
};
|
|
|
|
const response = await requestUrl({
|
|
url: url,
|
|
method: "POST",
|
|
body: formDataArray.buffer,
|
|
throw: false,
|
|
headers: headers,
|
|
});
|
|
|
|
if (response.status == 200) {
|
|
const jsonResponse = response.json;
|
|
return jsonResponse.short_url;
|
|
} else {
|
|
new NotifyUser(this.app, `Error uploading image: ${response.status}`).open();
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
new NotifyUser(this.app, `Exception while uploading image: ${error}`).open();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 创建新帖子
|
|
async createPost(title: string, content: string, category: number, tags: string[]): Promise<{ success: boolean; postId?: number; topicId?: number; error?: string }> {
|
|
const url = `${this.settings.baseUrl}/posts.json`;
|
|
const headers = {
|
|
"Content-Type": "application/json",
|
|
"Api-Key": this.settings.apiKey,
|
|
"Api-Username": this.settings.disUser,
|
|
};
|
|
|
|
try {
|
|
const response = await requestUrl({
|
|
url,
|
|
method: "POST",
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
title: title,
|
|
raw: content,
|
|
category: category,
|
|
tags: tags || []
|
|
}),
|
|
headers,
|
|
throw: false
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
const responseData = response.json;
|
|
if (responseData && responseData.id) {
|
|
return {
|
|
success: true,
|
|
postId: responseData.id,
|
|
topicId: responseData.topic_id
|
|
};
|
|
} else {
|
|
return {
|
|
success: false,
|
|
error: t('POST_ID_ERROR')
|
|
};
|
|
}
|
|
} else {
|
|
try {
|
|
const errorResponse = response.json;
|
|
if (errorResponse.errors && errorResponse.errors.length > 0) {
|
|
return {
|
|
success: false,
|
|
error: errorResponse.errors.join('\n')
|
|
};
|
|
}
|
|
if (errorResponse.error) {
|
|
return {
|
|
success: false,
|
|
error: errorResponse.error
|
|
};
|
|
}
|
|
} catch (parseError) {
|
|
return {
|
|
success: false,
|
|
error: `${t('PUBLISH_FAILED')} (${response.status})`
|
|
};
|
|
}
|
|
return {
|
|
success: false,
|
|
error: `${t('PUBLISH_FAILED')} (${response.status})`
|
|
};
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: `${t('PUBLISH_FAILED')}: ${error.message || t('UNKNOWN_ERROR')}`
|
|
};
|
|
}
|
|
}
|
|
|
|
// 更新帖子
|
|
async updatePost(postId: number, topicId: number, title: string, content: string, category: number, tags: string[]): Promise<{ success: boolean; error?: string }> {
|
|
const postEndpoint = `${this.settings.baseUrl}/posts/${postId}`;
|
|
const topicEndpoint = `${this.settings.baseUrl}/t/${topicId}`;
|
|
const headers = {
|
|
"Content-Type": "application/json",
|
|
"Api-Key": this.settings.apiKey,
|
|
"Api-Username": this.settings.disUser,
|
|
};
|
|
|
|
try {
|
|
// 首先更新帖子内容
|
|
const postResponse = await requestUrl({
|
|
url: postEndpoint,
|
|
method: "PUT",
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
raw: content,
|
|
edit_reason: "Updated from Obsidian"
|
|
}),
|
|
headers,
|
|
throw: false
|
|
});
|
|
|
|
if (postResponse.status !== 200) {
|
|
return {
|
|
success: false,
|
|
error: `${t('UPDATE_FAILED')} (${postResponse.status})`
|
|
};
|
|
}
|
|
|
|
// 然后更新主题(标题、分类和标签)
|
|
const topicResponse = await requestUrl({
|
|
url: topicEndpoint,
|
|
method: "PUT",
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
title: title,
|
|
category_id: category,
|
|
tags: tags || []
|
|
}),
|
|
headers,
|
|
throw: false
|
|
});
|
|
|
|
if (topicResponse.status === 200) {
|
|
return { success: true };
|
|
} else {
|
|
try {
|
|
const errorResponse = topicResponse.json;
|
|
if (errorResponse.errors && errorResponse.errors.length > 0) {
|
|
return {
|
|
success: false,
|
|
error: errorResponse.errors.join('\n')
|
|
};
|
|
}
|
|
if (errorResponse.error) {
|
|
return {
|
|
success: false,
|
|
error: errorResponse.error
|
|
};
|
|
}
|
|
} catch (parseError) {
|
|
return {
|
|
success: false,
|
|
error: `${t('UPDATE_FAILED')} (${topicResponse.status})`
|
|
};
|
|
}
|
|
return {
|
|
success: false,
|
|
error: `${t('UPDATE_FAILED')} (${topicResponse.status})`
|
|
};
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: `${t('UPDATE_FAILED')}: ${error.message || t('UNKNOWN_ERROR')}`
|
|
};
|
|
}
|
|
}
|
|
|
|
// 获取分类列表
|
|
async fetchCategories(): Promise<{ id: number; name: string }[]> {
|
|
try {
|
|
const url = `${this.settings.baseUrl}/categories.json?include_subcategories=true`;
|
|
const headers = {
|
|
"Api-Key": this.settings.apiKey,
|
|
"Api-Username": this.settings.disUser,
|
|
};
|
|
|
|
const response = await requestUrl({
|
|
url,
|
|
method: "GET",
|
|
headers,
|
|
throw: false
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
const data = response.json;
|
|
const categories: { id: number; name: string }[] = [];
|
|
|
|
if (data && data.category_list && data.category_list.categories) {
|
|
data.category_list.categories.forEach((category: any) => {
|
|
categories.push({
|
|
id: category.id,
|
|
name: category.name
|
|
});
|
|
|
|
// 添加子分类
|
|
if (category.subcategory_list) {
|
|
category.subcategory_list.forEach((subcategory: any) => {
|
|
categories.push({
|
|
id: subcategory.id,
|
|
name: `${category.name} > ${subcategory.name}`
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return categories;
|
|
} else {
|
|
new NotifyUser(this.app, `Error fetching categories: ${response.status}`).open();
|
|
return [];
|
|
}
|
|
} catch (error) {
|
|
new NotifyUser(this.app, `Exception while fetching categories: ${error}`).open();
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// 获取标签列表
|
|
async fetchTags(): Promise<{ name: string; canCreate: boolean }[]> {
|
|
try {
|
|
const url = `${this.settings.baseUrl}/tags.json`;
|
|
const headers = {
|
|
"Api-Key": this.settings.apiKey,
|
|
"Api-Username": this.settings.disUser,
|
|
};
|
|
|
|
const response = await requestUrl({
|
|
url,
|
|
method: "GET",
|
|
headers,
|
|
throw: false
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
const data = response.json;
|
|
const tags: { name: string; canCreate: boolean }[] = [];
|
|
|
|
if (data && data.tags) {
|
|
const canCreateTags = await this.checkCanCreateTags();
|
|
data.tags.forEach((tag: any) => {
|
|
tags.push({
|
|
name: tag.name,
|
|
canCreate: canCreateTags
|
|
});
|
|
});
|
|
}
|
|
|
|
return tags;
|
|
} else {
|
|
new NotifyUser(this.app, `Error fetching tags: ${response.status}`).open();
|
|
return [];
|
|
}
|
|
} catch (error) {
|
|
new NotifyUser(this.app, `Exception while fetching tags: ${error}`).open();
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// 检查是否可以创建标签
|
|
async checkCanCreateTags(): Promise<boolean> {
|
|
try {
|
|
const url = `${this.settings.baseUrl}/site.json`;
|
|
const headers = {
|
|
"Api-Key": this.settings.apiKey,
|
|
"Api-Username": this.settings.disUser,
|
|
};
|
|
|
|
const response = await requestUrl({
|
|
url,
|
|
method: "GET",
|
|
headers,
|
|
throw: false
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
const data = response.json;
|
|
if (data && data.can_create_tag) {
|
|
return data.can_create_tag;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 测试API密钥
|
|
async testApiKey(): Promise<{ success: boolean; message: string }> {
|
|
if (!this.settings.baseUrl || !this.settings.apiKey || !this.settings.disUser) {
|
|
return {
|
|
success: false,
|
|
message: t('MISSING_SETTINGS')
|
|
};
|
|
}
|
|
|
|
try {
|
|
const url = `${this.settings.baseUrl}/users/${this.settings.disUser}.json`;
|
|
const headers = {
|
|
"Api-Key": this.settings.apiKey,
|
|
"Api-Username": this.settings.disUser,
|
|
};
|
|
|
|
const response = await requestUrl({
|
|
url,
|
|
method: "GET",
|
|
headers,
|
|
throw: false
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
const data = response.json;
|
|
if (data && data.user) {
|
|
return {
|
|
success: true,
|
|
message: t('API_TEST_SUCCESS')
|
|
};
|
|
} else {
|
|
return {
|
|
success: false,
|
|
message: t('API_KEY_INVALID')
|
|
};
|
|
}
|
|
} else {
|
|
return {
|
|
success: false,
|
|
message: `${t('API_KEY_INVALID')} (${response.status})`
|
|
};
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: `${t('API_KEY_INVALID')}: ${error.message || t('UNKNOWN_ERROR')}`
|
|
};
|
|
}
|
|
}
|
|
|
|
// 获取特定主题的标签和分类信息
|
|
async fetchTopicInfo(topicId: number): Promise<{ tags: string[], categoryId?: number }> {
|
|
try {
|
|
const url = `${this.settings.baseUrl}/t/${topicId}.json`;
|
|
const headers = {
|
|
"Api-Key": this.settings.apiKey,
|
|
"Api-Username": this.settings.disUser,
|
|
};
|
|
|
|
const response = await requestUrl({
|
|
url,
|
|
method: "GET",
|
|
headers,
|
|
throw: false
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
const data = response.json;
|
|
return {
|
|
tags: data?.tags || [],
|
|
categoryId: data?.category_id
|
|
};
|
|
}
|
|
|
|
return { tags: [] };
|
|
} catch (error) {
|
|
new NotifyUser(this.app, `Exception while fetching topic info: ${error}`).open();
|
|
return { tags: [] };
|
|
}
|
|
}
|
|
|
|
// 获取特定主题的标签
|
|
async fetchTopicTags(topicId: number): Promise<string[]> {
|
|
try {
|
|
const topicInfo = await this.fetchTopicInfo(topicId);
|
|
return topicInfo.tags;
|
|
} catch (error) {
|
|
new NotifyUser(this.app, `Exception while fetching topic tags: ${error}`).open();
|
|
return [];
|
|
}
|
|
}
|
|
}
|