wumbo changes (proper mastodon API support and oauth login!)
This commit is contained in:
		
							parent
							
								
									b7c03381f7
								
							
						
					
					
						commit
						e17b26b075
					
				
					 20 changed files with 1935 additions and 1618 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,3 +1,4 @@ | ||||||
| **/.DS_Store | **/.DS_Store | ||||||
| node_modules/ | node_modules/ | ||||||
| dist/ | dist/ | ||||||
|  | # .secret/ | ||||||
|  |  | ||||||
							
								
								
									
										2362
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2362
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -4,7 +4,7 @@ | ||||||
|     "description": "social media for the galaxy-wide-web! 🌌", |     "description": "social media for the galaxy-wide-web! 🌌", | ||||||
|     "type": "module", |     "type": "module", | ||||||
|     "scripts": { |     "scripts": { | ||||||
|         "dev": "vite", |         "dev": "vite --host 0.0.0.0", | ||||||
|         "build": "vite build", |         "build": "vite build", | ||||||
|         "preview": "vite preview" |         "preview": "vite preview" | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
							
								
								
									
										117
									
								
								src/App.svelte
									
										
									
									
									
								
							
							
						
						
									
										117
									
								
								src/App.svelte
									
										
									
									
									
								
							|  | @ -1,84 +1,72 @@ | ||||||
| <script> | <script> | ||||||
|     import Feed from './Feed.svelte'; |     import Feed from './Feed.svelte'; | ||||||
|     import Error from './Error.svelte'; |     import { Client, server_types } from './client/client.js'; | ||||||
|     import Instance from './instance.js'; |  | ||||||
| 
 | 
 | ||||||
|     let ready = false; |     let ready = Client.get().app && Client.get().app.token; | ||||||
|     if (localStorage.getItem("fedi_host") && localStorage.getItem("fedi_token")) { | 
 | ||||||
|         Instance.setup( |     let auth_code = new URLSearchParams(location.search).get("code"); | ||||||
|             localStorage.getItem("fedi_host"), |     if (auth_code) { | ||||||
|             localStorage.getItem("fedi_token"), |         let client = Client.get(); | ||||||
|             true |         client.getToken(auth_code).then(() => { | ||||||
|         ).then(() => { |             client.save(); | ||||||
|             ready = true; |             location = location.origin; | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function log_in(event) { |     function log_in(event) { | ||||||
|  |         let client = Client.get(); | ||||||
|         event.preventDefault(); |         event.preventDefault(); | ||||||
|         localStorage.setItem("fedi_host", event.target.instance_host.value); |         const host = event.target.host.value; | ||||||
|         localStorage.setItem("fedi_token", event.target.session_token.value); | 
 | ||||||
|         location = location; |         client.init(host).then(() => { | ||||||
|  |             if (client.instance.type === server_types.INCOMPATIBLE) { | ||||||
|  |                 console.error("Server " + client.instance.host + " is not supported - " + client.instance.version); | ||||||
|  |                 alert("Sorry, this app is not compatible with " + client.instance.host + "!"); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             console.log("Server is \"" + client.instance.type + "\" (or compatible)."); | ||||||
|  |             client.save(); | ||||||
|  |             let oauth_url = client.getOAuthUrl(); | ||||||
|  |             location = oauth_url; | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|     function log_out() { |     function log_out() { | ||||||
|         localStorage.removeItem("fedi_host"); |         Client.get().logout().then(() => { | ||||||
|         localStorage.removeItem("fedi_token"); |             ready = false; | ||||||
|         location = location; |         }); | ||||||
|     } |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <header> | <header> | ||||||
|     <h1>space social</h1> |     <h1>space social</h1> | ||||||
|     <p>social media for the galaxy-wide-web! 🌌</p> |     <p>social media for the galaxy-wide-web! 🌌</p> | ||||||
|     <button id="log-out" on:click={log_out}>log out</button> |     <button id="logout" on:click={log_out}>log out</button> | ||||||
| </header> | </header> | ||||||
| 
 | 
 | ||||||
| <main> | <main> | ||||||
|     {#if ready} |     {#if ready} | ||||||
|         <Feed /> |         <Feed /> | ||||||
|     {:else if !Instance.get_instance().ok} |     {:else} | ||||||
|         <Error> |         <div class="pane"> | ||||||
|             <p>this app requires a <strong>instance host</strong> and <strong>session token</strong> to work! you may enter these below:</p> |             <form on:submit={log_in} id="login"> | ||||||
| 
 |                 <h1>welcome!</h1> | ||||||
|             <form on:submit={data => (log_in(data))}> |                 <p>please enter your instance domain to log in.</p> | ||||||
|                 <label for="instance host">instance host: </label> |                 <input type="text" id="host" aria-label="instance domain"> | ||||||
|                 <input type="text" id="instance_host"> |                 <button type="submit" id="login">log in</button> | ||||||
|                 <br> |  | ||||||
|                 <label for="session token">session token: </label> |  | ||||||
|                 <input type="password" id="session_token"> |  | ||||||
|                 <br> |  | ||||||
|                 <button type="submit" id="log-in">log in</button> |  | ||||||
|             </form> |             </form> | ||||||
| 
 | 
 | ||||||
|             <hr> |             <hr> | ||||||
| 
 | 
 | ||||||
|             <h4>how do i get these?</h4> |  | ||||||
|             <ul> |  | ||||||
|                 <li> |  | ||||||
|                     <strong>instance host</strong> refers to the domain of your fediverse instance. i.e. <code>ice.arimelody.me</code>. |  | ||||||
|                 </li> |  | ||||||
|                 <li> |  | ||||||
|                     a <strong>token</strong> is a unique code that grants applications permission to act on your behalf. |  | ||||||
|                     you can find it in your browser's cookies for your instance. |  | ||||||
|                     (instructions for <a href="https://support.mozilla.org/en-US/questions/1219653">firefox</a> |  | ||||||
|                     and <a href="https://superuser.com/questions/1715037/how-can-i-view-the-content-of-cookies-in-chrome">chrome</a>) |  | ||||||
|                 </li> |  | ||||||
|             </ul> |  | ||||||
| 
 |  | ||||||
|             <p><small> |             <p><small> | ||||||
|                 your login credentials will not be saved to an external server. |                 please note this is <strong><em>extremely experimental software</em></strong>; | ||||||
|                 they are required for communication with the fediverse instance, and are saved entirely within your browser. |  | ||||||
|                 a cleaner login flow will be built in the future. |  | ||||||
|             </small></p> |  | ||||||
|             <p><small> |  | ||||||
|                 oh yeah i should also probably mention this is <strong><em>extremely experimental software</em></strong>; |  | ||||||
|                 even if you use the exact same instance as me, you may encounter problems. |                 even if you use the exact same instance as me, you may encounter problems. | ||||||
|                 if that's all cool with you, welcome aboard! |                 if that's all cool with you, welcome aboard! | ||||||
|             </small></p> |             </small></p> | ||||||
| 
 | 
 | ||||||
|             <p>made with ❤️ by <a href="https://arimelody.me">ari melody</a>, 2024</p> |             <p>made with ❤️ by <a href="https://arimelody.me">ari melody</a>, 2024</p> | ||||||
|         </Error> |         </div> | ||||||
|     {/if} |     {/if} | ||||||
| </main> | </main> | ||||||
| 
 | 
 | ||||||
|  | @ -95,9 +83,12 @@ | ||||||
|         align-items: center; |         align-items: center; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     header h1 { | ||||||
|  |         margin: 0 16px 0 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     h1 { |     h1 { | ||||||
|         color: var(--accent); |         color: var(--accent); | ||||||
|         margin: 0 16px 0 0; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     main { |     main { | ||||||
|  | @ -105,6 +96,19 @@ | ||||||
|         margin: 0 auto; |         margin: 0 auto; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     div.pane { | ||||||
|  |         margin-top: 16px; | ||||||
|  |         padding: 20px 32px; | ||||||
|  |         border: 1px solid #8884; | ||||||
|  |         border-radius: 16px; | ||||||
|  |         background-color: var(--bg1); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     form#login { | ||||||
|  |         margin: 64px 0; | ||||||
|  |         text-align: center; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     a { |     a { | ||||||
|         color: var(--accent); |         color: var(--accent); | ||||||
|         text-decoration: none; |         text-decoration: none; | ||||||
|  | @ -115,14 +119,14 @@ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     input[type="text"], input[type="password"] { |     input[type="text"], input[type="password"] { | ||||||
|         margin-bottom: 8px; |         margin: 8px 0; | ||||||
|         padding: 4px 6px; |         padding: 4px 6px; | ||||||
|         font-family: inherit; |         font-family: inherit; | ||||||
|         border: none; |         border: none; | ||||||
|         border-radius: 8px; |         border-radius: 8px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     button#log-in, button#log-out { |     button#login, button#logout { | ||||||
|         margin-left: auto; |         margin-left: auto; | ||||||
|         padding: 8px 12px; |         padding: 8px 12px; | ||||||
|         font-size: 1em; |         font-size: 1em; | ||||||
|  | @ -134,17 +138,12 @@ | ||||||
|         transition: color .1s, background-color .1s; |         transition: color .1s, background-color .1s; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     button#log-in.active, button#log-out.active { |     button#login:hover, button#logout:hover { | ||||||
|         background: var(--accent); |  | ||||||
|         color: var(--bg0); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     button#log-in:hover, button#log-out:hover { |  | ||||||
|         color: var(--bg0); |         color: var(--bg0); | ||||||
|         background: var(--fg0); |         background: var(--fg0); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     button#log-in:active, button#log-out:active { |     button#login:active, button#logout:active { | ||||||
|         background: #0001; |         background: #0001; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -21,6 +21,4 @@ | ||||||
|         border-radius: 16px; |         border-radius: 16px; | ||||||
|         background-color: var(--bg1); |         background-color: var(--bg1); | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,9 @@ | ||||||
| <script> | <script> | ||||||
|     import Post from './post/Post.svelte'; |     import Post from './post/Post.svelte'; | ||||||
|     import Error from './Error.svelte'; |     import Error from './Error.svelte'; | ||||||
|     import Instance from './instance.js'; |     import { Client } from './client/client.js'; | ||||||
| 
 | 
 | ||||||
|  |     let client = Client.get(); | ||||||
|     let posts = []; |     let posts = []; | ||||||
|     let loading = false; |     let loading = false; | ||||||
| 
 | 
 | ||||||
|  | @ -13,15 +14,11 @@ | ||||||
|         loading = true; |         loading = true; | ||||||
| 
 | 
 | ||||||
|         let new_posts = []; |         let new_posts = []; | ||||||
|         if (posts.length === 0) new_posts = await Instance.get_timeline() |         if (posts.length === 0) new_posts = await client.getTimeline() | ||||||
|         else new_posts = await Instance.get_timeline(posts[posts.length - 1].id); |         else new_posts = await client.getTimeline(posts[posts.length - 1].id); | ||||||
|          |          | ||||||
|         if (!new_posts) { |         if (!new_posts) { | ||||||
|             error = `sorry! the frontend is unable to communicate with your server. |             console.error(`Failed to retrieve timeline posts.`); | ||||||
| 
 |  | ||||||
| this app is still in very early development, and is currently only built to support iceshrimp. |  | ||||||
| 
 |  | ||||||
| for more information, please consult the developer console.`; |  | ||||||
|             loading = false; |             loading = false; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | @ -37,13 +34,53 @@ for more information, please consult the developer console.`; | ||||||
|             load_posts(); |             load_posts(); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |     client.getPost("9upf5wtam363h1tp", 1).then(post => { | ||||||
|  |         posts = [...posts, post]; | ||||||
|  |         console.log(post); | ||||||
|  |     }); | ||||||
|  |     */ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div id="feed"> | <div id="feed"> | ||||||
|     {#if error} |     {#if error} | ||||||
|         <Error msg={error.replaceAll('\n', '<br>')} /> |         <Error msg={error.replaceAll('\n', '<br>')} /> | ||||||
|     {/if} |     {/if} | ||||||
|  |     {#if posts.length <= 0} | ||||||
|  |         <div class="loading"> | ||||||
|  |             <span>just a moment...</span> | ||||||
|  |         </div> | ||||||
|  |     {/if} | ||||||
|     {#each posts as post} |     {#each posts as post} | ||||||
|         <Post post={post} /> |         <Post post_data={post} /> | ||||||
|     {/each} |     {/each} | ||||||
| </div> | </div> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     .loading { | ||||||
|  |         width: 100%; | ||||||
|  |         height: 80vh; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: center; | ||||||
|  |         align-items: center; | ||||||
|  |         font-size: 2em; | ||||||
|  |         font-weight: bold; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .loading span { | ||||||
|  |         animation: pulse 1s infinite; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @keyframes pulse { | ||||||
|  |         from { | ||||||
|  |             opacity: .5; | ||||||
|  |         } | ||||||
|  |         50% { | ||||||
|  |             opacity: 1; | ||||||
|  |         } | ||||||
|  |         to { | ||||||
|  |             opacity: .5; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  |  | ||||||
							
								
								
									
										112
									
								
								src/api/firefish.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/api/firefish.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | ||||||
|  | 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 Client.get().api.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) { | ||||||
|  |     let user = new User(); | ||||||
|  |     user.id = data.id; | ||||||
|  |     user.nickname = data.display_name; | ||||||
|  |     user.username = data.username; | ||||||
|  |     user.host = data.fqn.split('@')[1]; | ||||||
|  |     user.avatar_url = data.avatar; | ||||||
|  |     user.emojis = []; | ||||||
|  |     data.emojis.forEach(emoji_data => { | ||||||
|  |         emoji_data.id = emoji_data.shortcode + '@' + user.host; | ||||||
|  |         emoji_data.name = emoji_data.shortcode; | ||||||
|  |         emoji_data.host = user.host; | ||||||
|  |         user.emojis.push(Client.get().api.parseEmoji(emoji_data)); | ||||||
|  |     }); | ||||||
|  |     Client.get().putCacheUser(user); | ||||||
|  |     return user; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function parseEmoji(data) { | ||||||
|  |     return mastodonAPI.parseEmoji(data); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getUser(user_id) { | ||||||
|  |     return mastodonAPI.getUser(user_id); | ||||||
|  | } | ||||||
							
								
								
									
										240
									
								
								src/api/mastodon.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								src/api/mastodon.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,240 @@ | ||||||
|  | import { Client } from '../client/client.js'; | ||||||
|  | import Post from '../post/post.js'; | ||||||
|  | import User from '../user/user.js'; | ||||||
|  | import Emoji from '../emoji.js'; | ||||||
|  | 
 | ||||||
|  | export async function createApp(host) { | ||||||
|  |     let form = new FormData(); | ||||||
|  |     form.append("client_name", "space social"); | ||||||
|  |     form.append("redirect_uris", `${location.origin}/callback`); | ||||||
|  |     form.append("scopes", "read write push"); | ||||||
|  |     form.append("website", "https://spacesocial.arimelody.me"); | ||||||
|  | 
 | ||||||
|  |     const res = await fetch(`https://${host}/api/v1/apps`, { | ||||||
|  |         method: "POST", | ||||||
|  |         body: form, | ||||||
|  |     }) | ||||||
|  |     .then(res => res.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |         console.error(error); | ||||||
|  |         return false; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (!res || !res.client_id) return false; | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         id: res.client_id, | ||||||
|  |         secret: res.client_secret, | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getOAuthUrl() { | ||||||
|  |     let client = Client.get(); | ||||||
|  |     return `https://${client.instance.host}/oauth/authorize` + | ||||||
|  |         `?client_id=${client.app.id}` + | ||||||
|  |         "&scope=read+write+push" + | ||||||
|  |         `&redirect_uri=${location.origin}/callback` + | ||||||
|  |         "&response_type=code"; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getToken(code) { | ||||||
|  |     let client = Client.get(); | ||||||
|  |     let form = new FormData(); | ||||||
|  |     form.append("client_id", client.app.id); | ||||||
|  |     form.append("client_secret", client.app.secret); | ||||||
|  |     form.append("redirect_uri", `${location.origin}/callback`); | ||||||
|  |     form.append("grant_type", "authorization_code"); | ||||||
|  |     form.append("code", code); | ||||||
|  |     form.append("scope", "read write push"); | ||||||
|  | 
 | ||||||
|  |     const res = await fetch(`https://${client.instance.host}/oauth/token`, { | ||||||
|  |         method: "POST", | ||||||
|  |         body: form, | ||||||
|  |     }) | ||||||
|  |     .then(res => res.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |         console.error(error); | ||||||
|  |         return false; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (!res || !res.access_token) return false; | ||||||
|  | 
 | ||||||
|  |     return res.access_token; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function revokeToken() { | ||||||
|  |     let client = Client.get(); | ||||||
|  |     let form = new FormData(); | ||||||
|  |     form.append("client_id", client.app.id); | ||||||
|  |     form.append("client_secret", client.app.secret); | ||||||
|  |     form.append("token", client.app.token); | ||||||
|  | 
 | ||||||
|  |     const res = await fetch(`https://${client.instance.host}/oauth/revoke`, { | ||||||
|  |         method: "POST", | ||||||
|  |         body: form, | ||||||
|  |     }) | ||||||
|  |     .catch(error => { | ||||||
|  |         console.error(error); | ||||||
|  |         return false; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (!res.ok) return false; | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getTimeline(last_post_id) { | ||||||
|  |     let client = Client.get(); | ||||||
|  |     let url = `https://${client.instance.host}/api/v1/timelines/home`; | ||||||
|  |     if (last_post_id) url += "?max_id=" + last_post_id; | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'GET', | ||||||
|  |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|  |     }).then(res => res.json()); | ||||||
|  | 
 | ||||||
|  |     let posts = []; | ||||||
|  |     for (let i in data) { | ||||||
|  |         const post_data = data[i]; | ||||||
|  |         const post = await client.api.parsePost(post_data, 1); | ||||||
|  |         if (!post) { | ||||||
|  |             if (post_data.id) { | ||||||
|  |                 console.warn("Failed to parse post #" + post_data.id); | ||||||
|  |             } else { | ||||||
|  |                 console.warn("Failed to parse post:"); | ||||||
|  |                 console.warn(post_data); | ||||||
|  |             } | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  |         posts.push(post); | ||||||
|  |     } | ||||||
|  |     return posts; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getPost(post_id, num_replies) { | ||||||
|  |     let client = Client.get(); | ||||||
|  |     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}`; | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'GET', | ||||||
|  |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|  |     }).then(res => res.json()); | ||||||
|  | 
 | ||||||
|  |     const post = await client.api.parsePost(data, num_replies); | ||||||
|  |     if (!post) { | ||||||
|  |         if (data.id) { | ||||||
|  |             console.warn("Failed to parse post data #" + data.id); | ||||||
|  |         } else { | ||||||
|  |             console.warn("Failed to parse post data:"); | ||||||
|  |             console.warn(data); | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |     return post; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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.content; | ||||||
|  |     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 Client.get().api.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) { | ||||||
|  |     let user = new User(); | ||||||
|  |     user.id = data.id; | ||||||
|  |     user.nickname = data.display_name; | ||||||
|  |     user.username = data.username; | ||||||
|  |     if (data.acct.includes('@')) | ||||||
|  |         user.host = data.acct.split('@')[1]; | ||||||
|  |     else | ||||||
|  |         user.host = data.username + '@' + Client.get().instance.host; | ||||||
|  |     user.avatar_url = data.avatar; | ||||||
|  |     user.emojis = []; | ||||||
|  |     data.emojis.forEach(emoji_data => { | ||||||
|  |         emoji_data.id = emoji_data.shortcode + '@' + user.host; | ||||||
|  |         emoji_data.name = emoji_data.shortcode; | ||||||
|  |         emoji_data.host = user.host; | ||||||
|  |         user.emojis.push(Client.get().api.parseEmoji(emoji_data)); | ||||||
|  |     }); | ||||||
|  |     Client.get().putCacheUser(user); | ||||||
|  |     return user; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function parseEmoji(data) { | ||||||
|  |     let emoji = new Emoji( | ||||||
|  |         data.id, | ||||||
|  |         data.name, | ||||||
|  |         data.host, | ||||||
|  |         data.url, | ||||||
|  |     ); | ||||||
|  |     Client.get().putCacheEmoji(emoji); | ||||||
|  |     return emoji; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getUser(user_id) { | ||||||
|  |     let client = Client.get(); | ||||||
|  |     let url = `https://${client.instance.host}/api/v1/accounts/${user_id}`; | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'GET', | ||||||
|  |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|  |     }).then(res => res.json()); | ||||||
|  | 
 | ||||||
|  |     const user = await Client.get().api.parseUser(data); | ||||||
|  |     if (!post) { | ||||||
|  |         if (data.id) { | ||||||
|  |             console.warn("Failed to parse user data #" + data.id); | ||||||
|  |         } else { | ||||||
|  |             console.warn("Failed to parse user data:"); | ||||||
|  |             console.warn(data); | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |     return user; | ||||||
|  | } | ||||||
							
								
								
									
										176
									
								
								src/client/client.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/client/client.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,176 @@ | ||||||
|  | import * as mastodonAPI from '../api/mastodon.js'; | ||||||
|  | import * as firefishAPI from '../api/firefish.js'; | ||||||
|  | import * as misskeyAPI from '../api/misskey.js'; | ||||||
|  | 
 | ||||||
|  | let client = false; | ||||||
|  | 
 | ||||||
|  | export const server_types = { | ||||||
|  |     INCOMPATIBLE: "incompatible", | ||||||
|  |     MASTODON: "mastodon", | ||||||
|  |     FIREFISH: "firefish", | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 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: "iceshrimp", type: server_types.FIREFISH }, | ||||||
|  |     { version: "sharkey", type: server_types.FIREFISH }, | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | export class Client { | ||||||
|  |     #api; | ||||||
|  |     instance; | ||||||
|  |     app; | ||||||
|  |     #cache; | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         this.instance = null; | ||||||
|  |         this.app = null; | ||||||
|  |         this.cache = { | ||||||
|  |             users: {}, | ||||||
|  |             emojis: {}, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static get() { | ||||||
|  |         if (client) return client; | ||||||
|  |         client = new Client(); | ||||||
|  |         window.peekie = client; | ||||||
|  |         client.load(); | ||||||
|  |         if (client.instance) client.#configureAPI(); | ||||||
|  |         return client; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async init(host) { | ||||||
|  |         if (host.startsWith("https://")) host = host.substring(8); | ||||||
|  |         const url = "https://" + host + "/api/v1/instance"; | ||||||
|  |         const data = await fetch(url).then(res => res.json()); | ||||||
|  |         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.#configureAPI(); | ||||||
|  |         this.app = await this.api.createApp(host); | ||||||
|  | 
 | ||||||
|  |         if (!this.app || !this.instance) { | ||||||
|  |             console.error("Failed to create app. Check the network logs for details."); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return this.auth; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #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() { | ||||||
|  |         return this.api.getOAuthUrl(this.app.secret); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getToken(code) { | ||||||
|  |         const token = await this.api.getToken(code); | ||||||
|  |         if (!token) { | ||||||
|  |             console.error("Failed to obtain access token"); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         this.app.token = token; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async revokeToken() { | ||||||
|  |         return await this.api.revokeToken(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getTimeline(last_post_id) { | ||||||
|  |         return await this.api.getTimeline(last_post_id); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getPost(post_id, num_replies) { | ||||||
|  |         return await this.api.getPost(post_id, num_replies); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     putCacheUser(user) { | ||||||
|  |         this.cache.users[user.id] = user; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getUser(user_id) { | ||||||
|  |         let user = this.cache.users[user_id]; | ||||||
|  |         if (user) return user; | ||||||
|  | 
 | ||||||
|  |         user = await this.api.getUser(user_id); | ||||||
|  |         if (user) return user; | ||||||
|  | 
 | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getUserByMention(mention) { | ||||||
|  |         let users = Object.values(this.cache.users); | ||||||
|  |         for (let i in users) { | ||||||
|  |             const user = users[i]; | ||||||
|  |             if (user.mention == mention) return user; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     putCacheEmoji(emoji) { | ||||||
|  |         this.cache.emojis[emoji.id] = emoji; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getEmoji(emoji_id) { | ||||||
|  |         let emoji = this.cache.emojis[emoji_id]; | ||||||
|  |         if (!emoji) return false; | ||||||
|  |         return emoji; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     save() { | ||||||
|  |         localStorage.setItem(save_name, JSON.stringify({ | ||||||
|  |             instance: this.instance, | ||||||
|  |             app: this.app, | ||||||
|  |         })); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     load() { | ||||||
|  |         let json = localStorage.getItem(save_name); | ||||||
|  |         if (!json) return false; | ||||||
|  |         let saved = JSON.parse(json); | ||||||
|  |         this.instance = saved.instance; | ||||||
|  |         this.app = saved.app; | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async logout() { | ||||||
|  |         if (!this.instance || !this.app || !this.api) return; | ||||||
|  |         if (!await this.revokeToken()) { | ||||||
|  |             console.warn("Failed to log out correctly; ditching the old tokens anyways."); | ||||||
|  |         } | ||||||
|  |         localStorage.removeItem(save_name); | ||||||
|  |         client = new Client(); | ||||||
|  |         console.log("Logged out successfully."); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										99
									
								
								src/emoji.js
									
										
									
									
									
								
							
							
						
						
									
										99
									
								
								src/emoji.js
									
										
									
									
									
								
							|  | @ -1,93 +1,50 @@ | ||||||
| import Instance from './instance.js'; | import { Client } from './client/client.js'; | ||||||
| 
 | 
 | ||||||
| const EMOJI_REGEX = /:[a-z0-9_\-]+:/g; | export const EMOJI_REGEX = /:[\w\-.]{0,32}@[\w\-.]{0,32}:/g; | ||||||
| 
 | export const EMOJI_NAME_REGEX = /:[\w\-.]{0,32}:/g; | ||||||
| let emoji_cache = []; |  | ||||||
| 
 | 
 | ||||||
| export default class Emoji { | export default class Emoji { | ||||||
|  |     id; | ||||||
|     name; |     name; | ||||||
|     host; |     host; | ||||||
|     url; |     url; | ||||||
|     width; |  | ||||||
|     height; |  | ||||||
| 
 | 
 | ||||||
|     static parse(data, host) { |     constructor(id, name, host, url) { | ||||||
|         const instance = Instance.get_instance(); |         this.id = id; | ||||||
|         let emoji = null; |         this.name = name; | ||||||
|         switch (instance.type) { |         this.host = host; | ||||||
|             case Instance.types.ICESHRIMP: |         this.url = url; | ||||||
|                 emoji = Emoji.#parse_iceshrimp(data); |  | ||||||
|                 break; |  | ||||||
|             case Instance.types.MASTODON: |  | ||||||
|                 emoji = Emoji.#parse_mastodon(data); |  | ||||||
|                 break; |  | ||||||
|             default: |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|         if (emoji !== null) emoji_cache.push(emoji); |  | ||||||
|         return emoji; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static #parse_iceshrimp(data, host) { |     get html() { | ||||||
|         let emoji = new Emoji() |         return `<img src="${this.url}" class="emoji" height="20" title="${this.name}" alt="${this.name}">`; | ||||||
|         emoji.name = data.name.substring(1, data.name.search('@')); |  | ||||||
|         emoji.host = host; |  | ||||||
|         emoji.url = data.url; |  | ||||||
|         emoji.width = data.width; |  | ||||||
|         emoji.height = data.height; |  | ||||||
|         return emoji; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static #parse_mastodon(data, host) { |  | ||||||
|         let emoji = new Emoji() |  | ||||||
|         emoji.name = data.shortcode; |  | ||||||
|         emoji.host = host; |  | ||||||
|         emoji.url = data.url; |  | ||||||
|         emoji.width = data.width; |  | ||||||
|         emoji.height = data.height; |  | ||||||
|         return emoji; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     get id() { |  | ||||||
|         return this.name + '@' + this.host; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function parse_text(text, ignore_instance) { | export function parseText(text, host) { | ||||||
|     if (!text) return text; |     if (!text) return text; | ||||||
|      |      | ||||||
|     let index = text.search(EMOJI_REGEX); |     let index = text.search(EMOJI_NAME_REGEX); | ||||||
|     if (index === -1) return text; |     if (index === -1) return text; | ||||||
|     index++; |  | ||||||
| 
 | 
 | ||||||
|     // find the emoji name
 |     // find the emoji name
 | ||||||
|     let length = 0; |     let length = text.substring(index + 1).search(':'); | ||||||
|     while (index + length < text.length && text[index + length] !== ':') length++; |     if (length <= 0) return text; | ||||||
|     let emoji_name = ':' + text.substring(index, index + length) + ':'; |     let emoji_name = text.substring(index + 1, index + length + 1); | ||||||
|  |     let emoji = Client.get().getEmoji(emoji_name + '@' + host); | ||||||
| 
 | 
 | ||||||
|     // does this emoji exist?
 |     if (emoji) { | ||||||
|     let emoji; |         return text.substring(0, index) + emoji.html + | ||||||
|     for (let cached in emoji_cache) { |             parseText(text.substring(index + length + 2), host); | ||||||
|         if (cached.id === emoji_name) { |  | ||||||
|             emoji = cached; |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 |     return text.substring(0, index + length + 1) + | ||||||
|     if (!emoji) return text.substring(0, index + length) + parse_text(text.substring(index + length)); |         parseText(text.substring(index + length + 1), host); | ||||||
| 
 |  | ||||||
|     // replace emoji code with <img>
 |  | ||||||
|     const img = `<img src="${emoji.url}" class="emoji" width="26" height="26" title=":${emoji_name}:" alt="${emoji_name}">`; |  | ||||||
|     return text.substring(0, index - 1) + img + |  | ||||||
|         parse(text.substring(index + length + 1), emojis, ignore_instance); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function parse_one(reaction, emojis) { | export function parseOne(emoji_id) { | ||||||
|     if (reaction == '❤') return '❤️'; // stupid heart unicode
 |     if (emoji_id == '❤') return '❤️'; // stupid heart unicode
 | ||||||
|     if (!reaction.startsWith(':') || !reaction.endsWith(':')) return reaction; |     if (EMOJI_REGEX.exec(':' + emoji_id + ':')) return emoji_id; | ||||||
|     for (let i = 0; i < emojis.length; i++) { |     let cached_emoji = Client.get().getEmoji(emoji_id); | ||||||
|         if (emojis[i].name == reaction.substring(1, reaction.length - 1)) |     if (!cached_emoji) return emoji_id; | ||||||
|             return `<img src="${emojis[i].url}" class="emoji" width="26" height="26" title="${reaction}" alt="${emojis[i].name}">`; |     return cached_emoji.html; | ||||||
|     } |  | ||||||
|     return reaction; |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										107
									
								
								src/instance.js
									
										
									
									
									
								
							
							
						
						
									
										107
									
								
								src/instance.js
									
										
									
									
									
								
							|  | @ -1,107 +0,0 @@ | ||||||
| import Post from './post/post.js'; |  | ||||||
| 
 |  | ||||||
| let instance; |  | ||||||
| 
 |  | ||||||
| const ERR_UNSUPPORTED = "Unsupported server"; |  | ||||||
| const ERR_SERVER_RESPONSE = "Unsupported response from the server"; |  | ||||||
| 
 |  | ||||||
| export default class Instance { |  | ||||||
|     #host; |  | ||||||
|     #token; |  | ||||||
|     #type; |  | ||||||
|     #secure; |  | ||||||
| 
 |  | ||||||
|     static types = { |  | ||||||
|         ICESHRIMP: "iceshrimp", |  | ||||||
|         MASTODON: "mastodon", |  | ||||||
|         MISSKEY: "misskey", |  | ||||||
|         AKKOMA: "akkoma", |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     static get_instance() { |  | ||||||
|         if (!instance) instance = new Instance(); |  | ||||||
|         return instance; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static async setup(host, token, secure) { |  | ||||||
|         instance = Instance.get_instance(); |  | ||||||
|         instance.host = host; |  | ||||||
|         instance.token = token; |  | ||||||
|         instance.secure = secure; |  | ||||||
|         await instance.#guess_type(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async #guess_type() { |  | ||||||
|         const url = instance.#proto + instance.host + "/api/v1/instance"; |  | ||||||
|         console.log("Snooping for instance information at " + url + "..."); |  | ||||||
|         const res = await fetch(url); |  | ||||||
|         const data = await res.json(); |  | ||||||
|         const version = data.version.toLowerCase(); |  | ||||||
| 
 |  | ||||||
|         instance.type = Instance.types.MASTODON; |  | ||||||
|         if (version.search("iceshrimp") !== -1) instance.type = Instance.types.ICESHRIMP; |  | ||||||
|         if (version.search("misskey") !== -1) instance.type = Instance.types.MISSKEY; |  | ||||||
|         if (version.search("akkoma") !== -1) instance.type = Instance.types.AKKOMA; |  | ||||||
| 
 |  | ||||||
|         console.log("Assumed server type to be \"" + instance.type + "\"."); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static async get_timeline(last_post_id) { |  | ||||||
|         let data = null; |  | ||||||
|         switch (instance.type) { |  | ||||||
|             case Instance.types.ICESHRIMP: |  | ||||||
|                 data = await instance.#get_timeline_iceshrimp(last_post_id); |  | ||||||
|                 break; |  | ||||||
|             case Instance.types.MASTODON: |  | ||||||
|                 data = await instance.#get_timeline_mastodon(last_post_id); |  | ||||||
|                 break; |  | ||||||
|             default: |  | ||||||
|                 console.error(ERR_UNSUPPORTED); |  | ||||||
|                 return null; |  | ||||||
|         } |  | ||||||
|         if (data.constructor != Array) { |  | ||||||
|             console.error(ERR_SERVER_RESPONSE); |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|         let posts = []; |  | ||||||
|         data.forEach(post_data => { |  | ||||||
|             const post = Post.parse(post_data); |  | ||||||
|             if (!post) return; |  | ||||||
|             posts = [...posts, post]; |  | ||||||
|         }); |  | ||||||
|         return posts; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async #get_timeline_iceshrimp(last_post_id) { |  | ||||||
|         let body = Object; |  | ||||||
|         if (last_post_id) body.untilId = last_post_id; |  | ||||||
|         const res = await fetch(this.#proto + this.host + "/api/notes/timeline", { |  | ||||||
|             method: 'POST', |  | ||||||
|             headers: { "Authorization": "Bearer " + this.token }, |  | ||||||
|             body: JSON.stringify(body) |  | ||||||
|         }); |  | ||||||
|         return await res.json(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async #get_timeline_mastodon(last_post_id) { |  | ||||||
|         let url = this.#proto + this.host + "/api/v1/timelines/home"; |  | ||||||
|         if (last_post_id) url += "?max_id=" + last_post_id; |  | ||||||
|         const res = await fetch(url, { |  | ||||||
|             method: 'GET', |  | ||||||
|             headers: { "Authorization": "Bearer " + this.token } |  | ||||||
|         }); |  | ||||||
|         return await res.json(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     get #proto() { |  | ||||||
|         if (this.secure) return "https://"; |  | ||||||
|         return "http://"; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static get ok() { |  | ||||||
|         if (!instance) return false; |  | ||||||
|         if (!instance.host) return false; |  | ||||||
|         if (!instance.token) return false; |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import './app.css'; | import './app.css'; | ||||||
| import App from './App.svelte'; | import App from './App.svelte'; | ||||||
| import Instance from './instance.js'; |  | ||||||
| 
 | 
 | ||||||
| const app = new App({ | const app = new App({ | ||||||
|     target: document.getElementById('app') |     target: document.getElementById('app') | ||||||
|  |  | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| <script> | <script> | ||||||
|     export let post; |     export let post; | ||||||
|  | 
 | ||||||
|  |     let rich_text; | ||||||
|  |     post.rich_text().then(res => {rich_text = res}); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-body"> | <div class="post-body"> | ||||||
|  | @ -7,7 +10,7 @@ | ||||||
|         <p class="post-warning"><strong>{post.warning}</strong></p> |         <p class="post-warning"><strong>{post.warning}</strong></p> | ||||||
|     {/if} |     {/if} | ||||||
|     {#if post.text} |     {#if post.text} | ||||||
|         <span class="post-text">{@html post.rich_text}</span> |         <span class="post-text">{@html rich_text}</span> | ||||||
|     {/if} |     {/if} | ||||||
|     <div class="post-media-container" data-count={post.files.length}> |     <div class="post-media-container" data-count={post.files.length}> | ||||||
|         {#each post.files as file} |         {#each post.files as file} | ||||||
|  | @ -40,6 +43,12 @@ | ||||||
|         word-wrap: break-word; |         word-wrap: break-word; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     .post-text :global(.emoji) { | ||||||
|  |         position: relative; | ||||||
|  |         top: 6px; | ||||||
|  |         height: 24px!important; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     .post-text :global(code) { |     .post-text :global(code) { | ||||||
|         font-size: 1.2em; |         font-size: 1.2em; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <script> | <script> | ||||||
|     import { parse_text as parse_emojis } from '../emoji.js'; |     import { parseText as parseEmojis } from '../emoji.js'; | ||||||
|     import { shorthand as short_time } from '../time.js'; |     import { shorthand as short_time } from '../time.js'; | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post; | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
| <div class="post-context"> | <div class="post-context"> | ||||||
|     <span class="post-context-icon">🔁</span> |     <span class="post-context-icon">🔁</span> | ||||||
|     <span class="post-context-action"> |     <span class="post-context-action"> | ||||||
|         <a href="/{post.user.mention}">{@html parse_emojis(post.user.name, post.user.emojis, true)}</a> boosted this post. |         <a href="/{post.user.mention}">{@html parseEmojis(post.user.name)}</a> boosted this post. | ||||||
|     </span> |     </span> | ||||||
|     <span class="post-context-time"> |     <span class="post-context-time"> | ||||||
|         <time title="{time_string}">{short_time(post.created_at)}</time> |         <time title="{time_string}">{short_time(post.created_at)}</time> | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ | ||||||
|         aria-label="{label}" |         aria-label="{label}" | ||||||
|         title="{title}" |         title="{title}" | ||||||
|         on:click={() => (play_sound(sound))}> |         on:click={() => (play_sound(sound))}> | ||||||
|         <span>{@html icon}</span> |         <span class="icon">{@html icon}</span> | ||||||
|         {#if count} |         {#if count} | ||||||
|             <span class="count">{count}</span> |             <span class="count">{count}</span> | ||||||
|         {/if} |         {/if} | ||||||
|  | @ -23,7 +23,11 @@ | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|     button { |     button { | ||||||
|  |         height: 32px; | ||||||
|         padding: 6px 8px; |         padding: 6px 8px; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |         gap: 4px; | ||||||
|         font-size: 1em; |         font-size: 1em; | ||||||
|         background: none; |         background: none; | ||||||
|         color: inherit; |         color: inherit; | ||||||
|  | @ -44,6 +48,14 @@ | ||||||
|         background: #0001; |         background: #0001; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     .icon { | ||||||
|  |         width: 20px; | ||||||
|  |         height: 20px; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: center; | ||||||
|  |         align-items: center; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     .count { |     .count { | ||||||
|         opacity: .5; |         opacity: .5; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <script> | <script> | ||||||
|     import { parse_text as parse_emojis } from '../emoji.js'; |     import { parseText as parseEmojis } from '../emoji.js'; | ||||||
|     import { shorthand as short_time } from '../time.js'; |     import { shorthand as short_time } from '../time.js'; | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post; | ||||||
|  | @ -13,7 +13,7 @@ | ||||||
|     </a> |     </a> | ||||||
|     <header class="post-header"> |     <header class="post-header"> | ||||||
|         <div class="post-user-info"> |         <div class="post-user-info"> | ||||||
|             <a href="/{post.user.mention}" class="name">{@html parse_emojis(post.user.name, post.user.emojis, true)}</a> |             <a href="/{post.user.mention}" class="name">{@html post.user.rich_name}</a> | ||||||
|             <span class="username">{post.user.mention}</span> |             <span class="username">{post.user.mention}</span> | ||||||
|         </div> |         </div> | ||||||
|         <div class="post-info"> |         <div class="post-info"> | ||||||
|  |  | ||||||
|  | @ -4,42 +4,42 @@ | ||||||
|     import Header from './Header.svelte'; |     import Header from './Header.svelte'; | ||||||
|     import Body from './Body.svelte'; |     import Body from './Body.svelte'; | ||||||
|     import FooterButton from './FooterButton.svelte'; |     import FooterButton from './FooterButton.svelte'; | ||||||
|     import { parse_one as parse_reaction } from '../emoji.js'; |     import { parseOne as parseEmoji } from '../emoji.js'; | ||||||
|     import { play_sound } from '../sound.js'; |     import { play_sound } from '../sound.js'; | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post_data; | ||||||
| 
 | 
 | ||||||
|     let post_context = undefined; |     let post_context = undefined; | ||||||
|     let _post = post; |     let post = post_data; | ||||||
|     let is_boost = false; |     let is_boost = false; | ||||||
|     if (_post.boost) { |     if (post_data.boost) { | ||||||
|         is_boost = true; |         is_boost = true; | ||||||
|         post_context = _post; |         post_context = post_data; | ||||||
|         _post = _post.boost; |         post = post_data.boost; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; |     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-container" aria-label={aria_label}> | <div class="post-container" aria-label={aria_label}> | ||||||
|     {#if _post.reply} |     {#if post.reply} | ||||||
|         <ReplyContext post={_post.reply} /> |         <ReplyContext post={post.reply} /> | ||||||
|     {/if} |     {/if} | ||||||
|     {#if is_boost && !post_context.text} |     {#if is_boost && !post_context.text} | ||||||
|         <BoostContext post={post_context} /> |         <BoostContext post={post_context} /> | ||||||
|     {/if} |     {/if} | ||||||
|     <article class="post"> |     <article class="post"> | ||||||
|         <Header post={_post} /> |         <Header post={post} /> | ||||||
|         <Body post={_post} /> |         <Body post={post} /> | ||||||
|         <footer class="post-footer"> |         <footer class="post-footer"> | ||||||
|             <div class="post-reactions"> |             <div class="post-reactions"> | ||||||
|                 {#each Object.keys(_post.reactions) as reaction} |                 {#each post.reactions as reaction} | ||||||
|                     <FooterButton icon={parse_reaction(reaction, _post.emojis)} type="reaction" bind:count={_post.reactions[reaction]} title={reaction} label="" /> |                     <FooterButton icon={reaction.emoji.html} type="reaction" bind:count={reaction.count} title={reaction.emoji.id} label="" /> | ||||||
|                 {/each} |                 {/each} | ||||||
|             </div> |             </div> | ||||||
|             <div class="post-actions"> |             <div class="post-actions"> | ||||||
|                 <FooterButton icon="🗨️" type="reply" label="Reply" bind:count={_post.reply_count} sound="post" /> |                 <FooterButton icon="🗨️" type="reply" label="Reply" bind:count={post.reply_count} sound="post" /> | ||||||
|                 <FooterButton icon="🔁" type="boost" label="Boost" bind:count={_post.boost_count} sound="boost" /> |                 <FooterButton icon="🔁" type="boost" label="Boost" bind:count={post.boost_count} sound="boost" /> | ||||||
|                 <FooterButton icon="⭐" type="favourite" label="Favourite" /> |                 <FooterButton icon="⭐" type="favourite" label="Favourite" /> | ||||||
|                 <FooterButton icon="😃" type="react" label="React" /> |                 <FooterButton icon="😃" type="react" label="React" /> | ||||||
|                 <FooterButton icon="🗣️" type="quote" label="Quote" /> |                 <FooterButton icon="🗣️" type="quote" label="Quote" /> | ||||||
|  | @ -63,17 +63,19 @@ | ||||||
|         background-color: var(--bg2); |         background-color: var(--bg2); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-reactions { |     :global(.post-reactions) { | ||||||
|         margin-top: 8px; |         margin-top: 16px; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-actions { |     :global(.post-actions) { | ||||||
|         margin-top: 8px; |         margin-top: 8px; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-container :global(.emoji) { |     .post-container :global(.emoji) { | ||||||
|         position: relative; |         height: 20px; | ||||||
|         top: 6px; |  | ||||||
|         height: 26px; |  | ||||||
|     } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
|     import Body from './Body.svelte'; |     import Body from './Body.svelte'; | ||||||
|     import FooterButton from './FooterButton.svelte'; |     import FooterButton from './FooterButton.svelte'; | ||||||
|     import Post from './Post.svelte'; |     import Post from './Post.svelte'; | ||||||
|     import { parse_text as parse_emojis, parse_one as parse_reaction } from '../emoji.js'; |     import { parseText as parseEmojis, parseOne as parseEmoji } from '../emoji.js'; | ||||||
|     import { shorthand as short_time } from '../time.js'; |     import { shorthand as short_time } from '../time.js'; | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post; | ||||||
|  | @ -24,7 +24,7 @@ | ||||||
|         <div class="post-header-container"> |         <div class="post-header-container"> | ||||||
|             <header class="post-header"> |             <header class="post-header"> | ||||||
|                 <div class="post-user-info"> |                 <div class="post-user-info"> | ||||||
|                     <a href="/{post.user.mention}" class="name">{@html parse_emojis(post.user.name, post.user.emojis, true)}</a> |                     <a href="/{post.user.mention}" class="name">{@html post.user.rich_name}</a> | ||||||
|                     <span class="username">{post.user.mention}</span> |                     <span class="username">{post.user.mention}</span> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="post-info"> |                 <div class="post-info"> | ||||||
|  | @ -39,8 +39,8 @@ | ||||||
| 
 | 
 | ||||||
|         <footer class="post-footer"> |         <footer class="post-footer"> | ||||||
|             <div class="post-reactions"> |             <div class="post-reactions"> | ||||||
|                 {#each Object.keys(post.reactions) as reaction} |                 {#each post.reactions as reaction} | ||||||
|                     <FooterButton icon={parse_reaction(reaction, post.emojis)} type="reaction" bind:count={post.reactions[reaction]} title={reaction} label="" /> |                     <FooterButton icon={reaction.emoji.html} type="reaction" bind:count={reaction.count} title={reaction.emoji.id} label="" /> | ||||||
|                 {/each} |                 {/each} | ||||||
|             </div> |             </div> | ||||||
|             <div class="post-actions"> |             <div class="post-actions"> | ||||||
|  |  | ||||||
							
								
								
									
										126
									
								
								src/post/post.js
									
										
									
									
									
								
							
							
						
						
									
										126
									
								
								src/post/post.js
									
										
									
									
									
								
							|  | @ -1,9 +1,5 @@ | ||||||
| import Instance from '../instance.js'; | import { Client, server_types } from '../client/client.js'; | ||||||
| import User from '../user/user.js'; | import { parseOne as parseEmoji, EMOJI_REGEX } from '../emoji.js'; | ||||||
| 
 |  | ||||||
| import { parse_one as parse_emoji } from '../emoji.js'; |  | ||||||
| 
 |  | ||||||
| let post_cache = Object; |  | ||||||
| 
 | 
 | ||||||
| export default class Post { | export default class Post { | ||||||
|     id; |     id; | ||||||
|  | @ -21,70 +17,7 @@ export default class Post { | ||||||
|     reply; |     reply; | ||||||
|     boost; |     boost; | ||||||
| 
 | 
 | ||||||
|     static resolve_id(id) { |     async rich_text() { | ||||||
|         return post_cache[id] || null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static parse(data) { |  | ||||||
|         const instance = Instance.get_instance(); |  | ||||||
|         let post = null; |  | ||||||
|         switch (instance.type) { |  | ||||||
|             case Instance.types.ICESHRIMP: |  | ||||||
|                 post = Post.#parse_iceshrimp(data); |  | ||||||
|                 break; |  | ||||||
|             case Instance.types.MASTODON: |  | ||||||
|                 post = Post.#parse_mastodon(data); |  | ||||||
|                 break; |  | ||||||
|             default: |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|         if (!post) { |  | ||||||
|             console.error("Error while parsing post data"); |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|         post_cache[post.id] = post; |  | ||||||
|         return post; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static #parse_iceshrimp(data) { |  | ||||||
|         let post = new Post() |  | ||||||
|         post.id = data.id; |  | ||||||
|         post.created_at = new Date(data.createdAt); |  | ||||||
|         post.user = User.parse(data.user); |  | ||||||
|         post.text = data.text; |  | ||||||
|         post.warning = data.cw; |  | ||||||
|         post.boost_count = data.renoteCount; |  | ||||||
|         post.reply_count = data.repliesCount; |  | ||||||
|         post.mentions = data.mentions; |  | ||||||
|         post.reactions = data.reactions; |  | ||||||
|         post.emojis = data.emojis; |  | ||||||
|         post.files = data.files; |  | ||||||
|         post.url = data.url; |  | ||||||
|         post.boost = data.renote ? Post.parse(data.renote) : null; |  | ||||||
|         post.reply = data.reply ? Post.parse(data.reply) : null; |  | ||||||
|         return post; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static #parse_mastodon(data) { |  | ||||||
|         let post = new Post() |  | ||||||
|         post.id = data.id; |  | ||||||
|         post.created_at = new Date(data.created_at); |  | ||||||
|         post.user = User.parse(data.account); |  | ||||||
|         post.text = data.content; |  | ||||||
|         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.emojis = data.emojis; |  | ||||||
|         post.files = data.media_attachments; |  | ||||||
|         post.url = data.url; |  | ||||||
|         post.boost = data.reblog ? Post.parse(data.reblog) : null; |  | ||||||
|         post.reply = data.in_reply_to_id ? Post.resolve_id(data.in_reply_to_id) : null; |  | ||||||
|         return post; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     get rich_text() { |  | ||||||
|         let text = this.text; |         let text = this.text; | ||||||
|         if (!text) return text; |         if (!text) return text; | ||||||
| 
 | 
 | ||||||
|  | @ -112,7 +45,8 @@ export default class Post { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // handle mentions
 |             // handle mentions
 | ||||||
|             if (allow_new && sample.match(/@[a-z0-9-_.]+@[a-z0-9-_.]+/g)) { |             // TODO: setup a better system for handling different server capabilities
 | ||||||
|  |             if (Client.get().instance.type !== server_types.MASTODON && allow_new && 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++; | ||||||
|  | @ -122,11 +56,11 @@ 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 = User.resolve_mention(mention); |                 let user = await Client.get().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.name + "</a>"; |                         '@' + user.username + '@' + user.host + "</a>"; | ||||||
|                     if (current) current.text += out; |                     if (current) current.text += out; | ||||||
|                     else response += out; |                     else response += out; | ||||||
|                 } else { |                 } else { | ||||||
|  | @ -136,33 +70,39 @@ export default class Post { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (Instance.get_instance().type !== Instance.types.MASTODON) { |             // handle links
 | ||||||
|                 // handle links
 |             if (Client.get().instance.type !== server_types.MASTODON && allow_new && sample.match(/^[a-z]{3,6}:\/\/[^\s]+/g)) { | ||||||
|                 if (allow_new && 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 (current) current.text += out; |                 else response += out; | ||||||
|                     else response += out; |                 index += length; | ||||||
|                     index += length; |                 continue; | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // handle emojis
 |             // handle emojis
 | ||||||
|             if (allow_new && sample.startsWith(':')) { |             if (allow_new && sample.match(/^:[\w\-.]{0,32}:/g)) { | ||||||
|                 // lookahead to next invalid emoji character
 |                 // find the emoji name
 | ||||||
|                 let look = sample.substring(1).search(/[^a-zA-Z0-9-_.]/g) + 1; |                 let length = text.substring(index + 1).search(':'); | ||||||
|                 // if it's ':', we can parse it
 |                 if (length <= 0) return text; | ||||||
|                 if (look !== 0 && sample[look] === ':') { |                 let emoji_name = text.substring(index + 1, index + length + 1); | ||||||
|                     let emoji_code = sample.substring(0, look + 1); |                 let emoji = Client.get().getEmoji(emoji_name + '@' + this.user.host); | ||||||
|                     let out = parse_emoji(emoji_code, this.emojis); | 
 | ||||||
|  |                 index += length + 2; | ||||||
|  | 
 | ||||||
|  |                 if (!emoji) { | ||||||
|  |                     let out = ':' + emoji_name + ':'; | ||||||
|                     if (current) current.text += out; |                     if (current) current.text += out; | ||||||
|                     else response += out; |                     else response += out; | ||||||
|                     index += emoji_code.length; |  | ||||||
|                     continue; |                     continue; | ||||||
|                 } |                 } | ||||||
|  | 
 | ||||||
|  |                 let out = emoji.html; | ||||||
|  |                 if (current) current.text += out; | ||||||
|  |                 else response += out; | ||||||
|  |                 continue; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // handle markdown
 |             // handle markdown
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,4 @@ | ||||||
| import Instance from '../instance.js'; | import { parseText as parseEmojis } from '../emoji.js'; | ||||||
| import Emoji from '../emoji.js'; |  | ||||||
| 
 |  | ||||||
| let user_cache = Object; |  | ||||||
| 
 | 
 | ||||||
| export default class User { | export default class User { | ||||||
|     id; |     id; | ||||||
|  | @ -11,66 +8,6 @@ export default class User { | ||||||
|     avatar_url; |     avatar_url; | ||||||
|     emojis; |     emojis; | ||||||
| 
 | 
 | ||||||
|     static resolve_id(id) { |  | ||||||
|         return user_cache[id]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static resolve_mention(mention) { |  | ||||||
|         for (let i = 0; i < Object.keys(user_cache).length; i++) { |  | ||||||
|             let user = user_cache[Object.keys(user_cache)[i]]; |  | ||||||
|             if (user.mention === mention) return user; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static parse(data) { |  | ||||||
|         const instance = Instance.get_instance(); |  | ||||||
|         let user = null; |  | ||||||
|         switch (instance.type) { |  | ||||||
|             case Instance.types.ICESHRIMP: |  | ||||||
|                 user = User.#parse_iceshrimp(data); |  | ||||||
|                 break; |  | ||||||
|             case Instance.types.MASTODON: |  | ||||||
|                 user = User.#parse_mastodon(data); |  | ||||||
|                 break; |  | ||||||
|             default: |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|         if (!user) { |  | ||||||
|             console.error("Error while parsing user data"); |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|         user_cache[user.id] = user; |  | ||||||
|         return user; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static #parse_iceshrimp(data) { |  | ||||||
|         let user = new User(); |  | ||||||
|         user.id = data.id; |  | ||||||
|         user.nickname = data.name; |  | ||||||
|         user.username = data.username; |  | ||||||
|         user.host = data.host || Instance.get_instance().host; |  | ||||||
|         user.avatar_url = data.avatarUrl; |  | ||||||
|         user.emojis = []; |  | ||||||
|         data.emojis.forEach(emoji => { |  | ||||||
|             user.emojis.push(Emoji.parse(emoji, user.host)); |  | ||||||
|         }); |  | ||||||
|         return user; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static #parse_mastodon(data) { |  | ||||||
|         let user = new User(); |  | ||||||
|         user.id = data.id; |  | ||||||
|         user.nickname = data.display_name; |  | ||||||
|         user.username = data.username; |  | ||||||
|         user.host = data.acct.search('@') ? data.acct.substring(data.acct.search('@') + 1) : instance.host; |  | ||||||
|         user.avatar_url = data.avatar; |  | ||||||
|         user.emojis = []; |  | ||||||
|         data.emojis.forEach(emoji => { |  | ||||||
|             user.emojis.push(Emoji.parse(emoji, user.host)); |  | ||||||
|         }); |  | ||||||
|         return user; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     get name() { |     get name() { | ||||||
|         return this.nickname || this.username; |         return this.nickname || this.username; | ||||||
|     } |     } | ||||||
|  | @ -80,4 +17,9 @@ export default class User { | ||||||
|         if (this.host) res += "@" + this.host; |         if (this.host) res += "@" + this.host; | ||||||
|         return res; |         return res; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     get rich_name() { | ||||||
|  |         if (!this.nickname) return this.username; | ||||||
|  |         return parseEmojis(this.nickname, this.host); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue