Merge 'capabilities API' (#1)
Reviewed-on: https://git.arimelody.me/ari/spacesocial/pulls/1
This commit is contained in:
		
						commit
						c6f3cd0d96
					
				
					 7 changed files with 204 additions and 246 deletions
				
			
		|  | @ -1,6 +1,6 @@ | ||||||
| <script> | <script> | ||||||
|     import Feed from './Feed.svelte'; |     import Feed from './Feed.svelte'; | ||||||
|     import { Client, server_types } from './client/client.js'; |     import { Client } from './client/client.js'; | ||||||
| 
 | 
 | ||||||
|     let ready = Client.get().app && Client.get().app.token; |     let ready = Client.get().app && Client.get().app.token; | ||||||
| 
 | 
 | ||||||
|  | @ -25,7 +25,6 @@ | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     function log_out() { |     function log_out() { | ||||||
|         Client.get().logout().then(() => { |         Client.get().logout().then(() => { | ||||||
|             ready = false; |             ready = false; | ||||||
|  |  | ||||||
|  | @ -1,98 +0,0 @@ | ||||||
| import { Client } from '../client/client.js'; |  | ||||||
| import Post from '../post/post.js'; |  | ||||||
| import User from '../user/user.js'; |  | ||||||
| import Emoji from '../emoji.js'; |  | ||||||
| 
 |  | ||||||
| import * as mastodonAPI from './mastodon.js'; |  | ||||||
| 
 |  | ||||||
| export async function createApp(host) { |  | ||||||
|     return await mastodonAPI.createApp(host); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function getOAuthUrl() { |  | ||||||
|     return mastodonAPI.getOAuthUrl(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getToken(code) { |  | ||||||
|     return await mastodonAPI.getToken(code); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function revokeToken() { |  | ||||||
|     return await mastodonAPI.revokeToken(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getTimeline(last_post_id) { |  | ||||||
|     return await mastodonAPI.getTimeline(last_post_id); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getPost(post_id, num_replies) { |  | ||||||
|     return await mastodonAPI.getPost(post_id, num_replies); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function parsePost(data, num_replies) { |  | ||||||
|     let client = Client.get(); |  | ||||||
|     let post = new Post() |  | ||||||
|     post.id = data.id; |  | ||||||
|     post.created_at = new Date(data.created_at); |  | ||||||
|     post.user = await Client.get().api.parseUser(data.account); |  | ||||||
|     post.text = data.text; |  | ||||||
|     post.warning = data.spoiler_text; |  | ||||||
|     post.boost_count = data.reblogs_count; |  | ||||||
|     post.reply_count = data.replies_count; |  | ||||||
|     post.mentions = data.mentions; |  | ||||||
|     post.reactions = data.reactions; |  | ||||||
|     post.files = data.media_attachments; |  | ||||||
|     post.url = data.url; |  | ||||||
|     post.reply = data.in_reply_to_id && num_replies > 0 ? await getPost(data.in_reply_to_id, num_replies - 1) : null; |  | ||||||
|     post.boost = data.reblog ? await parsePost(data.reblog, 1) : null; |  | ||||||
|     post.emojis = []; |  | ||||||
|     data.emojis.forEach(emoji_data => { |  | ||||||
|         let name = emoji_data.shortcode.split('@')[0]; |  | ||||||
|         post.emojis.push(Client.get().api.parseEmoji({ |  | ||||||
|             id: name + '@' + post.user.host, |  | ||||||
|             name: name, |  | ||||||
|             host: post.user.host, |  | ||||||
|             url: emoji_data.url, |  | ||||||
|         })); |  | ||||||
|     }); |  | ||||||
|     post.reactions = []; |  | ||||||
|     data.reactions.forEach(reaction_data => { |  | ||||||
|         if (/^[\w\-.@]+$/g.exec(reaction_data.name)) { |  | ||||||
|             let name = reaction_data.name.split('@')[0]; |  | ||||||
|             let host = reaction_data.name.includes('@') ? reaction_data.name.split('@')[1] : client.instance.host; |  | ||||||
|             post.reactions.push({ |  | ||||||
|                 count: reaction_data.count, |  | ||||||
|                 emoji: Client.get().api.parseEmoji({ |  | ||||||
|                     id: name + '@' + host, |  | ||||||
|                     name: name, |  | ||||||
|                     host: host, |  | ||||||
|                     url: reaction_data.url, |  | ||||||
|                 }), |  | ||||||
|                 me: reaction_data.me, |  | ||||||
|             }); |  | ||||||
|         } else { |  | ||||||
|             if (reaction_data.name == '❤') reaction_data.name = '❤️'; // stupid heart unicode
 |  | ||||||
|             post.reactions.push({ |  | ||||||
|                 count: reaction_data.count, |  | ||||||
|                 emoji: { |  | ||||||
|                     html: reaction_data.name, |  | ||||||
|                     name: reaction_data.name, |  | ||||||
|                 }, |  | ||||||
|                 me: reaction_data.me, |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
|     return post; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function parseUser(data) { |  | ||||||
|     return mastodonAPI.parseUser(data); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function parseEmoji(data) { |  | ||||||
|     return mastodonAPI.parseEmoji(data); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getUser(user_id) { |  | ||||||
|     return mastodonAPI.getUser(user_id); |  | ||||||
| } |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { Client } from '../client/client.js'; | import { Client } from '../client/client.js'; | ||||||
|  | import { capabilities } from '../client/instance.js'; | ||||||
| import Post from '../post/post.js'; | import Post from '../post/post.js'; | ||||||
| import User from '../user/user.js'; | import User from '../user/user.js'; | ||||||
| import Emoji from '../emoji.js'; | import Emoji from '../emoji.js'; | ||||||
|  | @ -94,14 +95,16 @@ export async function getTimeline(last_post_id) { | ||||||
|     let posts = []; |     let posts = []; | ||||||
|     for (let i in data) { |     for (let i in data) { | ||||||
|         const post_data = data[i]; |         const post_data = data[i]; | ||||||
|         const post = await client.api.parsePost(post_data, 1); |         const post = await parsePost(post_data, 1); | ||||||
|         if (!post) { |         if (!post) { | ||||||
|  |             if (post === null || post === undefined) { | ||||||
|                 if (post_data.id) { |                 if (post_data.id) { | ||||||
|                     console.warn("Failed to parse post #" + post_data.id); |                     console.warn("Failed to parse post #" + post_data.id); | ||||||
|                 } else { |                 } else { | ||||||
|                     console.warn("Failed to parse post:"); |                     console.warn("Failed to parse post:"); | ||||||
|                     console.warn(post_data); |                     console.warn(post_data); | ||||||
|                 } |                 } | ||||||
|  |             } | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
|         posts.push(post); |         posts.push(post); | ||||||
|  | @ -115,10 +118,12 @@ export async function getPost(post_id, num_replies) { | ||||||
|     const data = await fetch(url, { |     const data = await fetch(url, { | ||||||
|         method: 'GET', |         method: 'GET', | ||||||
|         headers: { "Authorization": "Bearer " + client.app.token } |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|     }).then(res => res.json()); |     }).then(res => { res.ok ? res.json() : false }); | ||||||
| 
 | 
 | ||||||
|     const post = await client.api.parsePost(data, num_replies); |     if (!data) return null; | ||||||
|     if (!post) { | 
 | ||||||
|  |     const post = await parsePost(data, num_replies); | ||||||
|  |     if (post === null || post === undefined) { | ||||||
|         if (data.id) { |         if (data.id) { | ||||||
|             console.warn("Failed to parse post data #" + data.id); |             console.warn("Failed to parse post data #" + data.id); | ||||||
|         } else { |         } else { | ||||||
|  | @ -133,29 +138,44 @@ export async function getPost(post_id, num_replies) { | ||||||
| export async function parsePost(data, num_replies) { | export async function parsePost(data, num_replies) { | ||||||
|     let client = Client.get(); |     let client = Client.get(); | ||||||
|     let post = new Post() |     let post = new Post() | ||||||
|  | 
 | ||||||
|     post.id = data.id; |     post.id = data.id; | ||||||
|     post.created_at = new Date(data.created_at); |     post.created_at = new Date(data.created_at); | ||||||
|     post.user = await Client.get().api.parseUser(data.account); |     post.user = await parseUser(data.account); | ||||||
|  | 
 | ||||||
|  |     if (client.instance.capabilities.includes(capabilities.MARKDOWN_CONTENT)) | ||||||
|  |         post.text = data.text; | ||||||
|  |     else | ||||||
|         post.text = data.content; |         post.text = data.content; | ||||||
|  | 
 | ||||||
|     post.warning = data.spoiler_text; |     post.warning = data.spoiler_text; | ||||||
|     post.boost_count = data.reblogs_count; |     post.boost_count = data.reblogs_count; | ||||||
|     post.reply_count = data.replies_count; |     post.reply_count = data.replies_count; | ||||||
|     post.mentions = data.mentions; |     post.mentions = data.mentions; | ||||||
|     post.reactions = data.reactions; |  | ||||||
|     post.files = data.media_attachments; |     post.files = data.media_attachments; | ||||||
|     post.url = data.url; |     post.url = data.url; | ||||||
|     post.reply = data.in_reply_to_id && num_replies > 0 ? await getPost(data.in_reply_to_id, num_replies - 1) : null; | 
 | ||||||
|  |     post.reply = null; | ||||||
|  |     if (data.in_reply_to_id && num_replies > 0) { | ||||||
|  |         post.reply = await getPost(data.in_reply_to_id, num_replies - 1); | ||||||
|  |         // if the post returns null, we probably don't have permission to read it.
 | ||||||
|  |         // we'll respect the thread's privacy, and leave it alone :)
 | ||||||
|  |         if (post.reply === null) return false; | ||||||
|  |     } | ||||||
|     post.boost = data.reblog ? await parsePost(data.reblog, 1) : null; |     post.boost = data.reblog ? await parsePost(data.reblog, 1) : null; | ||||||
|  | 
 | ||||||
|     post.emojis = []; |     post.emojis = []; | ||||||
|     data.emojis.forEach(emoji_data => { |     data.emojis.forEach(emoji_data => { | ||||||
|         let name = emoji_data.shortcode.split('@')[0]; |         let name = emoji_data.shortcode.split('@')[0]; | ||||||
|         post.emojis.push(Client.get().api.parseEmoji({ |         post.emojis.push(parseEmoji({ | ||||||
|             id: name + '@' + post.user.host, |             id: name + '@' + post.user.host, | ||||||
|             name: name, |             name: name, | ||||||
|             host: post.user.host, |             host: post.user.host, | ||||||
|             url: emoji_data.url, |             url: emoji_data.url, | ||||||
|         })); |         })); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     if (client.instance.capabilities.includes(capabilities.REACTIONS)) { | ||||||
|         post.reactions = []; |         post.reactions = []; | ||||||
|         data.reactions.forEach(reaction_data => { |         data.reactions.forEach(reaction_data => { | ||||||
|             if (/^[\w\-.@]+$/g.exec(reaction_data.name)) { |             if (/^[\w\-.@]+$/g.exec(reaction_data.name)) { | ||||||
|  | @ -163,7 +183,7 @@ export async function parsePost(data, num_replies) { | ||||||
|                 let host = reaction_data.name.includes('@') ? reaction_data.name.split('@')[1] : client.instance.host; |                 let host = reaction_data.name.includes('@') ? reaction_data.name.split('@')[1] : client.instance.host; | ||||||
|                 post.reactions.push({ |                 post.reactions.push({ | ||||||
|                     count: reaction_data.count, |                     count: reaction_data.count, | ||||||
|                 emoji: Client.get().api.parseEmoji({ |                     emoji: parseEmoji({ | ||||||
|                         id: name + '@' + host, |                         id: name + '@' + host, | ||||||
|                         name: name, |                         name: name, | ||||||
|                         host: host, |                         host: host, | ||||||
|  | @ -183,6 +203,7 @@ export async function parsePost(data, num_replies) { | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  |     } | ||||||
|     return post; |     return post; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -201,7 +222,7 @@ export async function parseUser(data) { | ||||||
|         emoji_data.id = emoji_data.shortcode + '@' + user.host; |         emoji_data.id = emoji_data.shortcode + '@' + user.host; | ||||||
|         emoji_data.name = emoji_data.shortcode; |         emoji_data.name = emoji_data.shortcode; | ||||||
|         emoji_data.host = user.host; |         emoji_data.host = user.host; | ||||||
|         user.emojis.push(Client.get().api.parseEmoji(emoji_data)); |         user.emojis.push(parseEmoji(emoji_data)); | ||||||
|     }); |     }); | ||||||
|     Client.get().putCacheUser(user); |     Client.get().putCacheUser(user); | ||||||
|     return user; |     return user; | ||||||
|  | @ -226,8 +247,8 @@ export async function getUser(user_id) { | ||||||
|         headers: { "Authorization": "Bearer " + client.app.token } |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|     }).then(res => res.json()); |     }).then(res => res.json()); | ||||||
| 
 | 
 | ||||||
|     const user = await Client.get().api.parseUser(data); |     const user = await parseUser(data); | ||||||
|     if (!post) { |     if (user === null || user === undefined) { | ||||||
|         if (data.id) { |         if (data.id) { | ||||||
|             console.warn("Failed to parse user data #" + data.id); |             console.warn("Failed to parse user data #" + data.id); | ||||||
|         } else { |         } else { | ||||||
|  | @ -1,27 +1,11 @@ | ||||||
| import * as mastodonAPI from '../api/mastodon.js'; | import { Instance, server_types } from './instance.js'; | ||||||
| import * as firefishAPI from '../api/firefish.js'; | import * as api from './api.js'; | ||||||
| 
 | 
 | ||||||
| let client = false; | let client = false; | ||||||
| 
 | 
 | ||||||
| export const server_types = { |  | ||||||
|     INCOMPATIBLE: "incompatible", |  | ||||||
|     MASTODON: "mastodon", |  | ||||||
|     FIREFISH: "firefish", |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const save_name = "spacesocial"; | const save_name = "spacesocial"; | ||||||
| 
 | 
 | ||||||
| const versions_types = [ |  | ||||||
|     { version: "mastodon", type: server_types.MASTODON }, |  | ||||||
|     { version: "glitchsoc", type: server_types.MASTODON }, |  | ||||||
|     { version: "chuckya", type: server_types.MASTODON }, |  | ||||||
|     { version: "firefish", type: server_types.FIREFISH }, |  | ||||||
|     { version: "iceshrimp", type: server_types.FIREFISH }, |  | ||||||
|     { version: "sharkey", type: server_types.FIREFISH }, |  | ||||||
| ]; |  | ||||||
| 
 |  | ||||||
| export class Client { | export class Client { | ||||||
|     #api; |  | ||||||
|     instance; |     instance; | ||||||
|     app; |     app; | ||||||
|     #cache; |     #cache; | ||||||
|  | @ -40,44 +24,29 @@ export class Client { | ||||||
|         client = new Client(); |         client = new Client(); | ||||||
|         window.peekie = client; |         window.peekie = client; | ||||||
|         client.load(); |         client.load(); | ||||||
|         if (client.instance && client.instance !== server_types.INCOMPATIBLE) |  | ||||||
|             client.#configureAPI(); |  | ||||||
|         return client; |         return client; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async init(host) { |     async init(host) { | ||||||
|         if (host.startsWith("https://")) host = host.substring(8); |         if (host.startsWith("https://")) host = host.substring(8); | ||||||
|         const url = `https://${host}/api/v1/instance`; |         const url = `https://${host}/api/v1/instance`; | ||||||
|         const data = await fetch(url).then(res => res.json()).catch(error => { console.log(error) }); |         const data = await fetch(url).then(res => res.json()).catch(error => { console.error(error) }); | ||||||
|         if (!data) { |         if (!data) { | ||||||
|             console.error(`Failed to connect to ${host}`); |             console.error(`Failed to connect to ${host}`); | ||||||
|             alert(`Failed to connect to ${host}! Please try again later.`); |             alert(`Failed to connect to ${host}! Please try again later.`); | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|         this.instance = { |  | ||||||
|             host: host, |  | ||||||
|             version: data.version, |  | ||||||
|             type: server_types.INCOMPATIBLE, |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         for (let index in versions_types) { |  | ||||||
|             const pair = versions_types[index]; |  | ||||||
|             if (data.version.toLowerCase().includes(pair.version)) { |  | ||||||
|                 this.instance.type = pair.type; |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|          |          | ||||||
|  |         this.instance = new Instance(host, data.version); | ||||||
|         if (this.instance.type == server_types.INCOMPATIBLE) { |         if (this.instance.type == server_types.INCOMPATIBLE) { | ||||||
|             console.error(`Server ${host} is not supported - ${data.version}`); |             console.error(`Server ${host} is not supported - ${data.version}`); | ||||||
|             alert(`Sorry, this app is not compatible with ${host}!`); |             alert(`Sorry, this app is not compatible with ${host}!`); | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         console.log(`Server is "${client.instance.type}" (or compatible).`); |         console.log(`Server is "${this.instance.type}" (or compatible) with capabilities: [${this.instance.capabilities}].`); | ||||||
| 
 | 
 | ||||||
|         this.#configureAPI(); |         this.app = await api.createApp(host); | ||||||
|         this.app = await this.api.createApp(host); |  | ||||||
| 
 | 
 | ||||||
|         if (!this.app || !this.instance) { |         if (!this.app || !this.instance) { | ||||||
|             console.error("Failed to create app. Check the network logs for details."); |             console.error("Failed to create app. Check the network logs for details."); | ||||||
|  | @ -89,29 +58,12 @@ export class Client { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     #configureAPI() { |  | ||||||
|         switch (this.instance.type) { |  | ||||||
|             case server_types.MASTODON: |  | ||||||
|                 this.api = mastodonAPI; |  | ||||||
|                 break; |  | ||||||
|             case server_types.FIREFISH: |  | ||||||
|                 this.api = firefishAPI; |  | ||||||
|                 break; |  | ||||||
|             /* not opening that can of worms for a while |  | ||||||
|             case server_types.MISSKEY: |  | ||||||
|                 this.api = misskeyAPI; |  | ||||||
|                 break; */ |  | ||||||
|             default: |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     getOAuthUrl() { |     getOAuthUrl() { | ||||||
|         return this.api.getOAuthUrl(this.app.secret); |         return api.getOAuthUrl(this.app.secret); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getToken(code) { |     async getToken(code) { | ||||||
|         const token = await this.api.getToken(code); |         const token = await api.getToken(code); | ||||||
|         if (!token) { |         if (!token) { | ||||||
|             console.error("Failed to obtain access token"); |             console.error("Failed to obtain access token"); | ||||||
|             return false; |             return false; | ||||||
|  | @ -120,15 +72,15 @@ export class Client { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async revokeToken() { |     async revokeToken() { | ||||||
|         return await this.api.revokeToken(); |         return await api.revokeToken(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getTimeline(last_post_id) { |     async getTimeline(last_post_id) { | ||||||
|         return await this.api.getTimeline(last_post_id); |         return await api.getTimeline(last_post_id); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getPost(post_id, num_replies) { |     async getPost(post_id, num_replies) { | ||||||
|         return await this.api.getPost(post_id, num_replies); |         return await api.getPost(post_id, num_replies); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     putCacheUser(user) { |     putCacheUser(user) { | ||||||
|  | @ -139,7 +91,7 @@ export class Client { | ||||||
|         let user = this.cache.users[user_id]; |         let user = this.cache.users[user_id]; | ||||||
|         if (user) return user; |         if (user) return user; | ||||||
| 
 | 
 | ||||||
|         user = await this.api.getUser(user_id); |         user = await api.getUser(user_id); | ||||||
|         if (user) return user; |         if (user) return user; | ||||||
| 
 | 
 | ||||||
|         return false; |         return false; | ||||||
|  | @ -166,7 +118,10 @@ export class Client { | ||||||
| 
 | 
 | ||||||
|     save() { |     save() { | ||||||
|         localStorage.setItem(save_name, JSON.stringify({ |         localStorage.setItem(save_name, JSON.stringify({ | ||||||
|             instance: this.instance, |             instance: { | ||||||
|  |                 host: this.instance.host, | ||||||
|  |                 version: this.instance.version, | ||||||
|  |             }, | ||||||
|             app: this.app, |             app: this.app, | ||||||
|         })); |         })); | ||||||
|     } |     } | ||||||
|  | @ -175,13 +130,13 @@ export class Client { | ||||||
|         let json = localStorage.getItem(save_name); |         let json = localStorage.getItem(save_name); | ||||||
|         if (!json) return false; |         if (!json) return false; | ||||||
|         let saved = JSON.parse(json); |         let saved = JSON.parse(json); | ||||||
|         this.instance = saved.instance; |         this.instance = new Instance(saved.instance.host, saved.instance.version); | ||||||
|         this.app = saved.app; |         this.app = saved.app; | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async logout() { |     async logout() { | ||||||
|         if (!this.instance || !this.app || !this.api) return; |         if (!this.instance || !this.app) return; | ||||||
|         if (!await this.revokeToken()) { |         if (!await this.revokeToken()) { | ||||||
|             console.warn("Failed to log out correctly; ditching the old tokens anyways."); |             console.warn("Failed to log out correctly; ditching the old tokens anyways."); | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										68
									
								
								src/client/instance.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/client/instance.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,68 @@ | ||||||
|  | export const server_types = { | ||||||
|  |     UNSUPPORTED: "unsupported", | ||||||
|  |     MASTODON: "mastodon", | ||||||
|  |     GLITCHSOC: "glitchsoc", | ||||||
|  |     CHUCKYA: "chuckya", | ||||||
|  |     FIREFISH: "firefish", | ||||||
|  |     ICESHRIMP: "iceshrimp", | ||||||
|  |     SHARKEY: "sharkey", | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const capabilities = { | ||||||
|  |     MARKDOWN_CONTENT: "mdcontent", | ||||||
|  |     REACTIONS: "reactions", | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export class Instance { | ||||||
|  |     host; | ||||||
|  |     version; | ||||||
|  |     capabilities; | ||||||
|  |     type = server_types.UNSUPPORTED; | ||||||
|  | 
 | ||||||
|  |     constructor(host, version) { | ||||||
|  |         this.host = host; | ||||||
|  |         this.version = version; | ||||||
|  |         this.#setType(version); | ||||||
|  |         this.capabilities = this.#getCapabilities(this.type); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #setType(version) { | ||||||
|  |         if (version.constructor !== String) return; | ||||||
|  |         let version_lower = version.toLowerCase(); | ||||||
|  |         for (let i = 1; i < Object.keys(server_types).length; i++) { | ||||||
|  |             const check_type = Object.values(server_types)[i]; | ||||||
|  |             if (version_lower.includes(check_type)) { | ||||||
|  |                 this.type = check_type; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (this.type === server_types.UNSUPPORTED) return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #getCapabilities(type) { | ||||||
|  |         let c = []; | ||||||
|  |         switch (type) { | ||||||
|  |             case server_types.MASTODON: | ||||||
|  |                 break; | ||||||
|  |             case server_types.GLITCHSOC: | ||||||
|  |                 c.push(capabilities.REACTIONS); | ||||||
|  |                 break; | ||||||
|  |             case server_types.CHUCKYA: | ||||||
|  |                 c.push(capabilities.REACTIONS); | ||||||
|  |                 break; | ||||||
|  |             case server_types.FIREFISH: | ||||||
|  |                 c.push(capabilities.REACTIONS); | ||||||
|  |                 break; | ||||||
|  |             case server_types.ICESHRIMP: | ||||||
|  |                 c.push(capabilities.MARKDOWN_CONTENT); | ||||||
|  |                 c.push(capabilities.REACTIONS); | ||||||
|  |                 break; | ||||||
|  |             case server_types.SHARKEY: | ||||||
|  |                 c.push(capabilities.REACTIONS); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         return c; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -49,6 +49,12 @@ | ||||||
|         height: 24px!important; |         height: 24px!important; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     .post-text :global(blockquote) { | ||||||
|  |         border-left: 4px solid #8888; | ||||||
|  |         padding: .2em 2em; | ||||||
|  |         margin: .8em 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     .post-text :global(code) { |     .post-text :global(code) { | ||||||
|         font-size: 1.2em; |         font-size: 1.2em; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { Client, server_types } from '../client/client.js'; | import { Client } from '../client/client.js'; | ||||||
|  | import { capabilities, server_types } from '../client/instance.js'; | ||||||
| import { parseOne as parseEmoji, EMOJI_REGEX } from '../emoji.js'; | import { parseOne as parseEmoji, EMOJI_REGEX } from '../emoji.js'; | ||||||
| 
 | 
 | ||||||
| export default class Post { | export default class Post { | ||||||
|  | @ -20,33 +21,36 @@ export default class Post { | ||||||
|     async rich_text() { |     async rich_text() { | ||||||
|         let text = this.text; |         let text = this.text; | ||||||
|         if (!text) return text; |         if (!text) return text; | ||||||
|  |         let client = Client.get(); | ||||||
| 
 | 
 | ||||||
|         const markdown_tokens = [ |         const markdown_tokens = [ | ||||||
|             { tag: "pre", token: "```" }, |             { tag: "pre", token: "```" }, | ||||||
|             { tag: "code", token: "`" }, |             { tag: "code", token: "`" }, | ||||||
|             { tag: "strong", token: "**", regex: /\*{2}/g }, |             { tag: "strong", token: "**" }, | ||||||
|             { tag: "strong", token: "__" }, |             { tag: "strong", token: "__" }, | ||||||
|             { tag: "em", token: "*", regex: /\*/g }, |             { tag: "em", token: "*" }, | ||||||
|             { tag: "em", token: "_" }, |             { tag: "em", token: "_" }, | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|         let response = ""; |         let response = ""; | ||||||
|         let current; |         let md_layer; | ||||||
|         let index = 0; |         let index = 0; | ||||||
|         while (index < text.length) { |         while (index < text.length) { | ||||||
|             let sample = text.substring(index); |             let sample = text.substring(index); | ||||||
|             let allow_new = !current || !current.nostack; |             let md_nostack = !(md_layer && md_layer.nostack); | ||||||
| 
 | 
 | ||||||
|             // handle newlines
 |             // handle newlines
 | ||||||
|             if (allow_new && sample.startsWith('\n')) { |             if (md_nostack && sample.startsWith('\n')) { | ||||||
|                 response += "<br>"; |                 response += "<br>"; | ||||||
|                 index++; |                 index++; | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // handle mentions
 |             // handle mentions
 | ||||||
|             // TODO: setup a better system for handling different server capabilities
 |             if (client.instance.capabilities.includes(capabilities.MARKDOWN_CONTENT) | ||||||
|             if (Client.get().instance.type !== server_types.MASTODON && allow_new && sample.match(/^@[\w\-.]+@[\w\-.]+/g)) { |                 && md_nostack | ||||||
|  |                 && sample.match(/^@[\w\-.]+@[\w\-.]+/g) | ||||||
|  |             ) { | ||||||
|                 // find end of the mention
 |                 // find end of the mention
 | ||||||
|                 let length = 1; |                 let length = 1; | ||||||
|                 while (index + length < text.length && /[a-z0-9-_.]/.test(text[index + length])) length++; |                 while (index + length < text.length && /[a-z0-9-_.]/.test(text[index + length])) length++; | ||||||
|  | @ -56,12 +60,12 @@ export default class Post { | ||||||
|                 let mention = text.substring(index, index + length); |                 let mention = text.substring(index, index + length); | ||||||
| 
 | 
 | ||||||
|                 // attempt to resolve mention to a user
 |                 // attempt to resolve mention to a user
 | ||||||
|                 let user = await Client.get().getUserByMention(mention); |                 let user = await client.getUserByMention(mention); | ||||||
|                 if (user) { |                 if (user) { | ||||||
|                     const out = `<a href="/${user.mention}" class="mention">` + |                     const out = `<a href="/${user.mention}" class="mention">` + | ||||||
|                         `<img src="${user.avatar_url}" class="mention-avatar" width="20" height="20">` + |                         `<img src="${user.avatar_url}" class="mention-avatar" width="20" height="20">` + | ||||||
|                         '@' + user.username + '@' + user.host + "</a>"; |                         '@' + user.username + '@' + user.host + "</a>"; | ||||||
|                     if (current) current.text += out; |                     if (md_layer) md_layer.text += out; | ||||||
|                     else response += out; |                     else response += out; | ||||||
|                 } else { |                 } else { | ||||||
|                     response += mention; |                     response += mention; | ||||||
|  | @ -71,72 +75,75 @@ export default class Post { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // handle links
 |             // handle links
 | ||||||
|             if (Client.get().instance.type !== server_types.MASTODON && allow_new && sample.match(/^[a-z]{3,6}:\/\/[^\s]+/g)) { |             if (client.instance.capabilities.includes(capabilities.MARKDOWN_CONTENT) | ||||||
|  |                 && md_nostack | ||||||
|  |                 && sample.match(/^[a-z]{3,6}:\/\/[^\s]+/g) | ||||||
|  |             ) { | ||||||
|                 // get length of link
 |                 // get length of link
 | ||||||
|                 let length = text.substring(index).search(/\s|$/g); |                 let length = text.substring(index).search(/\s|$/g); | ||||||
|                 let url = text.substring(index, index + length); |                 let url = text.substring(index, index + length); | ||||||
|                 let out = `<a href="${url}">${url}</a>`; |                 let out = `<a href="${url}">${url}</a>`; | ||||||
|                 if (current) current.text += out; |                 if (md_layer) md_layer.text += out; | ||||||
|                 else response += out; |                 else response += out; | ||||||
|                 index += length; |                 index += length; | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // handle emojis
 |             // handle emojis
 | ||||||
|             if (allow_new && sample.match(/^:[\w\-.]{0,32}:/g)) { |             if (md_nostack && sample.match(/^:[\w\-.]{0,32}:/g)) { | ||||||
|                 // find the emoji name
 |                 // find the emoji name
 | ||||||
|                 let length = text.substring(index + 1).search(':'); |                 let length = text.substring(index + 1).search(':'); | ||||||
|                 if (length <= 0) return text; |                 if (length <= 0) return text; | ||||||
|                 let emoji_name = text.substring(index + 1, index + length + 1); |                 let emoji_name = text.substring(index + 1, index + length + 1); | ||||||
|                 let emoji = Client.get().getEmoji(emoji_name + '@' + this.user.host); |                 let emoji = client.getEmoji(emoji_name + '@' + this.user.host); | ||||||
| 
 | 
 | ||||||
|                 index += length + 2; |                 index += length + 2; | ||||||
| 
 | 
 | ||||||
|                 if (!emoji) { |                 if (!emoji) { | ||||||
|                     let out = ':' + emoji_name + ':'; |                     let out = ':' + emoji_name + ':'; | ||||||
|                     if (current) current.text += out; |                     if (md_layer) md_layer.text += out; | ||||||
|                     else response += out; |                     else response += out; | ||||||
|                     continue; |                     continue; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 let out = emoji.html; |                 let out = emoji.html; | ||||||
|                 if (current) current.text += out; |                 if (md_layer) md_layer.text += out; | ||||||
|                 else response += out; |                 else response += out; | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // handle markdown
 |             // handle markdown
 | ||||||
|             // TODO: handle misskey-flavoured markdown
 |             // TODO: handle misskey-flavoured markdown(?)
 | ||||||
|             if (current) { |             if (md_layer) { | ||||||
|                 // try to pop stack
 |                 // try to pop layer
 | ||||||
|                 if (sample.startsWith(current.token)) { |                 if (sample.startsWith(md_layer.token)) { | ||||||
|                     index += current.token.length; |                     index += md_layer.token.length; | ||||||
|                     let out = `<${current.tag}>${current.text}</${current.tag}>`; |                     let out = `<${md_layer.tag}>${md_layer.text}</${md_layer.tag}>`; | ||||||
|                     if (current.token === '```') |                     if (md_layer.token === '```') | ||||||
|                         out = `<code><pre>${current.text}</pre></code>`; |                         out = `<code><pre>${md_layer.text}</pre></code>`; | ||||||
|                     if (current.parent) current.parent.text += out; |                     if (md_layer.parent) md_layer.parent.text += out; | ||||||
|                     else response += out; |                     else response += out; | ||||||
|                     current = current.parent; |                     md_layer = md_layer.parent; | ||||||
|                 } else { |                 } else { | ||||||
|                     current.text += sample[0]; |                     md_layer.text += sample[0]; | ||||||
|                     index++; |                     index++; | ||||||
|                 } |                 } | ||||||
|             } else if (allow_new) { |             } else if (md_nostack) { | ||||||
|                 // can we add to stack?
 |                 // should we add a layer?
 | ||||||
|                 let pushed = false; |                 let pushed = false; | ||||||
|                 for (let i = 0; i < markdown_tokens.length; i++) { |                 for (let i = 0; i < markdown_tokens.length; i++) { | ||||||
|                     let item = markdown_tokens[i]; |                     let item = markdown_tokens[i]; | ||||||
|                     if (sample.startsWith(item.token)) { |                     if (sample.startsWith(item.token)) { | ||||||
|                         let new_current = { |                         let new_md_layer = { | ||||||
|                             token: item.token, |                             token: item.token, | ||||||
|                             tag: item.tag, |                             tag: item.tag, | ||||||
|                             text: "", |                             text: "", | ||||||
|                             parent: current, |                             parent: md_layer, | ||||||
|                         }; |                         }; | ||||||
|                         if (item.token === '```' || item.token === '`') new_current.nostack = true; |                         if (item.token === '```' || item.token === '`') new_md_layer.nostack = true; | ||||||
|                         current = new_current; |                         md_layer = new_md_layer; | ||||||
|                         pushed = true; |                         pushed = true; | ||||||
|                         index += current.token.length; |                         index += md_layer.token.length; | ||||||
|                         break; |                         break; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  | @ -148,11 +155,11 @@ export default class Post { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // destroy the remaining stack
 |         // destroy the remaining stack
 | ||||||
|         while (current) { |         while (md_layer) { | ||||||
|             let out = current.token + current.text; |             let out = md_layer.token + md_layer.text; | ||||||
|             if (current.parent) current.parent.text += out; |             if (md_layer.parent) md_layer.parent.text += out; | ||||||
|             else response += out; |             else response += out; | ||||||
|             current = current.parent; |             md_layer = md_layer.parent; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return response; |         return response; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue