wumbo changes (proper mastodon API support and oauth login!)
This commit is contained in:
parent
b7c03381f7
commit
e17b26b075
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
**/.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
# .secret/
|
||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "spacesocial-client",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "spacesocial-client",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"description": "social media for the galaxy-wide-web! 🌌",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
|
|
117
src/App.svelte
117
src/App.svelte
|
@ -1,84 +1,72 @@
|
|||
<script>
|
||||
import Feed from './Feed.svelte';
|
||||
import Error from './Error.svelte';
|
||||
import Instance from './instance.js';
|
||||
import { Client, server_types } from './client/client.js';
|
||||
|
||||
let ready = false;
|
||||
if (localStorage.getItem("fedi_host") && localStorage.getItem("fedi_token")) {
|
||||
Instance.setup(
|
||||
localStorage.getItem("fedi_host"),
|
||||
localStorage.getItem("fedi_token"),
|
||||
true
|
||||
).then(() => {
|
||||
ready = true;
|
||||
let ready = Client.get().app && Client.get().app.token;
|
||||
|
||||
let auth_code = new URLSearchParams(location.search).get("code");
|
||||
if (auth_code) {
|
||||
let client = Client.get();
|
||||
client.getToken(auth_code).then(() => {
|
||||
client.save();
|
||||
location = location.origin;
|
||||
});
|
||||
}
|
||||
|
||||
function log_in(event) {
|
||||
let client = Client.get();
|
||||
event.preventDefault();
|
||||
localStorage.setItem("fedi_host", event.target.instance_host.value);
|
||||
localStorage.setItem("fedi_token", event.target.session_token.value);
|
||||
location = location;
|
||||
const host = event.target.host.value;
|
||||
|
||||
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() {
|
||||
localStorage.removeItem("fedi_host");
|
||||
localStorage.removeItem("fedi_token");
|
||||
location = location;
|
||||
Client.get().logout().then(() => {
|
||||
ready = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<h1>space social</h1>
|
||||
<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>
|
||||
|
||||
<main>
|
||||
{#if ready}
|
||||
<Feed />
|
||||
{:else if !Instance.get_instance().ok}
|
||||
<Error>
|
||||
<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={data => (log_in(data))}>
|
||||
<label for="instance host">instance host: </label>
|
||||
<input type="text" id="instance_host">
|
||||
<br>
|
||||
<label for="session token">session token: </label>
|
||||
<input type="password" id="session_token">
|
||||
<br>
|
||||
<button type="submit" id="log-in">log in</button>
|
||||
{:else}
|
||||
<div class="pane">
|
||||
<form on:submit={log_in} id="login">
|
||||
<h1>welcome!</h1>
|
||||
<p>please enter your instance domain to log in.</p>
|
||||
<input type="text" id="host" aria-label="instance domain">
|
||||
<button type="submit" id="login">log in</button>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
your login credentials will not be saved to an external server.
|
||||
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>;
|
||||
please note this is <strong><em>extremely experimental software</em></strong>;
|
||||
even if you use the exact same instance as me, you may encounter problems.
|
||||
if that's all cool with you, welcome aboard!
|
||||
</small></p>
|
||||
|
||||
<p>made with ❤️ by <a href="https://arimelody.me">ari melody</a>, 2024</p>
|
||||
</Error>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
|
@ -95,9 +83,12 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0 16px 0 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--accent);
|
||||
margin: 0 16px 0 0;
|
||||
}
|
||||
|
||||
main {
|
||||
|
@ -105,6 +96,19 @@
|
|||
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 {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
|
@ -115,14 +119,14 @@
|
|||
}
|
||||
|
||||
input[type="text"], input[type="password"] {
|
||||
margin-bottom: 8px;
|
||||
margin: 8px 0;
|
||||
padding: 4px 6px;
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
button#log-in, button#log-out {
|
||||
button#login, button#logout {
|
||||
margin-left: auto;
|
||||
padding: 8px 12px;
|
||||
font-size: 1em;
|
||||
|
@ -134,17 +138,12 @@
|
|||
transition: color .1s, background-color .1s;
|
||||
}
|
||||
|
||||
button#log-in.active, button#log-out.active {
|
||||
background: var(--accent);
|
||||
color: var(--bg0);
|
||||
}
|
||||
|
||||
button#log-in:hover, button#log-out:hover {
|
||||
button#login:hover, button#logout:hover {
|
||||
color: var(--bg0);
|
||||
background: var(--fg0);
|
||||
}
|
||||
|
||||
button#log-in:active, button#log-out:active {
|
||||
button#login:active, button#logout:active {
|
||||
background: #0001;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,4 @@
|
|||
border-radius: 16px;
|
||||
background-color: var(--bg1);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
import Post from './post/Post.svelte';
|
||||
import Error from './Error.svelte';
|
||||
import Instance from './instance.js';
|
||||
import { Client } from './client/client.js';
|
||||
|
||||
let client = Client.get();
|
||||
let posts = [];
|
||||
let loading = false;
|
||||
|
||||
|
@ -13,15 +14,11 @@
|
|||
loading = true;
|
||||
|
||||
let new_posts = [];
|
||||
if (posts.length === 0) new_posts = await Instance.get_timeline()
|
||||
else new_posts = await Instance.get_timeline(posts[posts.length - 1].id);
|
||||
if (posts.length === 0) new_posts = await client.getTimeline()
|
||||
else new_posts = await client.getTimeline(posts[posts.length - 1].id);
|
||||
|
||||
if (!new_posts) {
|
||||
error = `sorry! the frontend is unable to communicate with your server.
|
||||
|
||||
this app is still in very early development, and is currently only built to support iceshrimp.
|
||||
|
||||
for more information, please consult the developer console.`;
|
||||
console.error(`Failed to retrieve timeline posts.`);
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
@ -37,13 +34,53 @@ for more information, please consult the developer console.`;
|
|||
load_posts();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
client.getPost("9upf5wtam363h1tp", 1).then(post => {
|
||||
posts = [...posts, post];
|
||||
console.log(post);
|
||||
});
|
||||
*/
|
||||
</script>
|
||||
|
||||
<div id="feed">
|
||||
{#if error}
|
||||
<Error msg={error.replaceAll('\n', '<br>')} />
|
||||
{/if}
|
||||
{#if posts.length <= 0}
|
||||
<div class="loading">
|
||||
<span>just a moment...</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#each posts as post}
|
||||
<Post post={post} />
|
||||
<Post post_data={post} />
|
||||
{/each}
|
||||
</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;
|
||||
|
||||
let emoji_cache = [];
|
||||
export const EMOJI_REGEX = /:[\w\-.]{0,32}@[\w\-.]{0,32}:/g;
|
||||
export const EMOJI_NAME_REGEX = /:[\w\-.]{0,32}:/g;
|
||||
|
||||
export default class Emoji {
|
||||
id;
|
||||
name;
|
||||
host;
|
||||
url;
|
||||
width;
|
||||
height;
|
||||
|
||||
static parse(data, host) {
|
||||
const instance = Instance.get_instance();
|
||||
let emoji = null;
|
||||
switch (instance.type) {
|
||||
case Instance.types.ICESHRIMP:
|
||||
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;
|
||||
constructor(id, name, host, url) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.host = host;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
static #parse_iceshrimp(data, host) {
|
||||
let emoji = new Emoji()
|
||||
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;
|
||||
get html() {
|
||||
return `<img src="${this.url}" class="emoji" height="20" title="${this.name}" alt="${this.name}">`;
|
||||
}
|
||||
}
|
||||
|
||||
export function parse_text(text, ignore_instance) {
|
||||
export function parseText(text, host) {
|
||||
if (!text) return text;
|
||||
|
||||
let index = text.search(EMOJI_REGEX);
|
||||
let index = text.search(EMOJI_NAME_REGEX);
|
||||
if (index === -1) return text;
|
||||
index++;
|
||||
|
||||
// find the emoji name
|
||||
let length = 0;
|
||||
while (index + length < text.length && text[index + length] !== ':') length++;
|
||||
let emoji_name = ':' + text.substring(index, index + length) + ':';
|
||||
let length = text.substring(index + 1).search(':');
|
||||
if (length <= 0) return text;
|
||||
let emoji_name = text.substring(index + 1, index + length + 1);
|
||||
let emoji = Client.get().getEmoji(emoji_name + '@' + host);
|
||||
|
||||
// does this emoji exist?
|
||||
let emoji;
|
||||
for (let cached in emoji_cache) {
|
||||
if (cached.id === emoji_name) {
|
||||
emoji = cached;
|
||||
break;
|
||||
if (emoji) {
|
||||
return text.substring(0, index) + emoji.html +
|
||||
parseText(text.substring(index + length + 2), host);
|
||||
}
|
||||
}
|
||||
|
||||
if (!emoji) return text.substring(0, index + length) + parse_text(text.substring(index + length));
|
||||
|
||||
// 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);
|
||||
return text.substring(0, index + length + 1) +
|
||||
parseText(text.substring(index + length + 1), host);
|
||||
}
|
||||
|
||||
export function parse_one(reaction, emojis) {
|
||||
if (reaction == '❤') return '❤️'; // stupid heart unicode
|
||||
if (!reaction.startsWith(':') || !reaction.endsWith(':')) return reaction;
|
||||
for (let i = 0; i < emojis.length; i++) {
|
||||
if (emojis[i].name == reaction.substring(1, reaction.length - 1))
|
||||
return `<img src="${emojis[i].url}" class="emoji" width="26" height="26" title="${reaction}" alt="${emojis[i].name}">`;
|
||||
}
|
||||
return reaction;
|
||||
export function parseOne(emoji_id) {
|
||||
if (emoji_id == '❤') return '❤️'; // stupid heart unicode
|
||||
if (EMOJI_REGEX.exec(':' + emoji_id + ':')) return emoji_id;
|
||||
let cached_emoji = Client.get().getEmoji(emoji_id);
|
||||
if (!cached_emoji) return emoji_id;
|
||||
return cached_emoji.html;
|
||||
}
|
||||
|
|
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 from './App.svelte';
|
||||
import Instance from './instance.js';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app')
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<script>
|
||||
export let post;
|
||||
|
||||
let rich_text;
|
||||
post.rich_text().then(res => {rich_text = res});
|
||||
</script>
|
||||
|
||||
<div class="post-body">
|
||||
|
@ -7,7 +10,7 @@
|
|||
<p class="post-warning"><strong>{post.warning}</strong></p>
|
||||
{/if}
|
||||
{#if post.text}
|
||||
<span class="post-text">{@html post.rich_text}</span>
|
||||
<span class="post-text">{@html rich_text}</span>
|
||||
{/if}
|
||||
<div class="post-media-container" data-count={post.files.length}>
|
||||
{#each post.files as file}
|
||||
|
@ -40,6 +43,12 @@
|
|||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.post-text :global(.emoji) {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
height: 24px!important;
|
||||
}
|
||||
|
||||
.post-text :global(code) {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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';
|
||||
|
||||
export let post;
|
||||
|
@ -10,7 +10,7 @@
|
|||
<div class="post-context">
|
||||
<span class="post-context-icon">🔁</span>
|
||||
<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 class="post-context-time">
|
||||
<time title="{time_string}">{short_time(post.created_at)}</time>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
aria-label="{label}"
|
||||
title="{title}"
|
||||
on:click={() => (play_sound(sound))}>
|
||||
<span>{@html icon}</span>
|
||||
<span class="icon">{@html icon}</span>
|
||||
{#if count}
|
||||
<span class="count">{count}</span>
|
||||
{/if}
|
||||
|
@ -23,7 +23,11 @@
|
|||
|
||||
<style>
|
||||
button {
|
||||
height: 32px;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
font-size: 1em;
|
||||
background: none;
|
||||
color: inherit;
|
||||
|
@ -44,6 +48,14 @@
|
|||
background: #0001;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.count {
|
||||
opacity: .5;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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';
|
||||
|
||||
export let post;
|
||||
|
@ -13,7 +13,7 @@
|
|||
</a>
|
||||
<header class="post-header">
|
||||
<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>
|
||||
</div>
|
||||
<div class="post-info">
|
||||
|
|
|
@ -4,42 +4,42 @@
|
|||
import Header from './Header.svelte';
|
||||
import Body from './Body.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';
|
||||
|
||||
export let post;
|
||||
export let post_data;
|
||||
|
||||
let post_context = undefined;
|
||||
let _post = post;
|
||||
let post = post_data;
|
||||
let is_boost = false;
|
||||
if (_post.boost) {
|
||||
if (post_data.boost) {
|
||||
is_boost = true;
|
||||
post_context = _post;
|
||||
_post = _post.boost;
|
||||
post_context = post_data;
|
||||
post = post_data.boost;
|
||||
}
|
||||
|
||||
let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at;
|
||||
</script>
|
||||
|
||||
<div class="post-container" aria-label={aria_label}>
|
||||
{#if _post.reply}
|
||||
<ReplyContext post={_post.reply} />
|
||||
{#if post.reply}
|
||||
<ReplyContext post={post.reply} />
|
||||
{/if}
|
||||
{#if is_boost && !post_context.text}
|
||||
<BoostContext post={post_context} />
|
||||
{/if}
|
||||
<article class="post">
|
||||
<Header post={_post} />
|
||||
<Body post={_post} />
|
||||
<Header post={post} />
|
||||
<Body post={post} />
|
||||
<footer class="post-footer">
|
||||
<div class="post-reactions">
|
||||
{#each Object.keys(_post.reactions) as reaction}
|
||||
<FooterButton icon={parse_reaction(reaction, _post.emojis)} type="reaction" bind:count={_post.reactions[reaction]} title={reaction} label="" />
|
||||
{#each post.reactions as reaction}
|
||||
<FooterButton icon={reaction.emoji.html} type="reaction" bind:count={reaction.count} title={reaction.emoji.id} label="" />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="post-actions">
|
||||
<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="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="favourite" label="Favourite" />
|
||||
<FooterButton icon="😃" type="react" label="React" />
|
||||
<FooterButton icon="🗣️" type="quote" label="Quote" />
|
||||
|
@ -63,17 +63,19 @@
|
|||
background-color: var(--bg2);
|
||||
}
|
||||
|
||||
.post-reactions {
|
||||
margin-top: 8px;
|
||||
:global(.post-reactions) {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.post-actions {
|
||||
:global(.post-actions) {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.post-container :global(.emoji) {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
height: 26px;
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import Body from './Body.svelte';
|
||||
import FooterButton from './FooterButton.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';
|
||||
|
||||
export let post;
|
||||
|
@ -24,7 +24,7 @@
|
|||
<div class="post-header-container">
|
||||
<header class="post-header">
|
||||
<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>
|
||||
</div>
|
||||
<div class="post-info">
|
||||
|
@ -39,8 +39,8 @@
|
|||
|
||||
<footer class="post-footer">
|
||||
<div class="post-reactions">
|
||||
{#each Object.keys(post.reactions) as reaction}
|
||||
<FooterButton icon={parse_reaction(reaction, post.emojis)} type="reaction" bind:count={post.reactions[reaction]} title={reaction} label="" />
|
||||
{#each post.reactions as reaction}
|
||||
<FooterButton icon={reaction.emoji.html} type="reaction" bind:count={reaction.count} title={reaction.emoji.id} label="" />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="post-actions">
|
||||
|
|
108
src/post/post.js
108
src/post/post.js
|
@ -1,9 +1,5 @@
|
|||
import Instance from '../instance.js';
|
||||
import User from '../user/user.js';
|
||||
|
||||
import { parse_one as parse_emoji } from '../emoji.js';
|
||||
|
||||
let post_cache = Object;
|
||||
import { Client, server_types } from '../client/client.js';
|
||||
import { parseOne as parseEmoji, EMOJI_REGEX } from '../emoji.js';
|
||||
|
||||
export default class Post {
|
||||
id;
|
||||
|
@ -21,70 +17,7 @@ export default class Post {
|
|||
reply;
|
||||
boost;
|
||||
|
||||
static resolve_id(id) {
|
||||
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() {
|
||||
async rich_text() {
|
||||
let text = this.text;
|
||||
if (!text) return text;
|
||||
|
||||
|
@ -112,7 +45,8 @@ export default class Post {
|
|||
}
|
||||
|
||||
// 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
|
||||
let length = 1;
|
||||
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);
|
||||
|
||||
// attempt to resolve mention to a user
|
||||
let user = User.resolve_mention(mention);
|
||||
let user = await Client.get().getUserByMention(mention);
|
||||
if (user) {
|
||||
const out = `<a href="/${user.mention}" class="mention">` +
|
||||
`<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;
|
||||
else response += out;
|
||||
} else {
|
||||
|
@ -136,9 +70,8 @@ export default class Post {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (Instance.get_instance().type !== Instance.types.MASTODON) {
|
||||
// handle links
|
||||
if (allow_new && sample.match(/^[a-z]{3,6}:\/\/[^\s]+/g)) {
|
||||
if (Client.get().instance.type !== server_types.MASTODON && allow_new && sample.match(/^[a-z]{3,6}:\/\/[^\s]+/g)) {
|
||||
// get length of link
|
||||
let length = text.substring(index).search(/\s|$/g);
|
||||
let url = text.substring(index, index + length);
|
||||
|
@ -148,21 +81,28 @@ export default class Post {
|
|||
index += length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// handle emojis
|
||||
if (allow_new && sample.startsWith(':')) {
|
||||
// lookahead to next invalid emoji character
|
||||
let look = sample.substring(1).search(/[^a-zA-Z0-9-_.]/g) + 1;
|
||||
// if it's ':', we can parse it
|
||||
if (look !== 0 && sample[look] === ':') {
|
||||
let emoji_code = sample.substring(0, look + 1);
|
||||
let out = parse_emoji(emoji_code, this.emojis);
|
||||
if (allow_new && sample.match(/^:[\w\-.]{0,32}:/g)) {
|
||||
// find the emoji name
|
||||
let length = text.substring(index + 1).search(':');
|
||||
if (length <= 0) return text;
|
||||
let emoji_name = text.substring(index + 1, index + length + 1);
|
||||
let emoji = Client.get().getEmoji(emoji_name + '@' + this.user.host);
|
||||
|
||||
index += length + 2;
|
||||
|
||||
if (!emoji) {
|
||||
let out = ':' + emoji_name + ':';
|
||||
if (current) current.text += out;
|
||||
else response += out;
|
||||
index += emoji_code.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
let out = emoji.html;
|
||||
if (current) current.text += out;
|
||||
else response += out;
|
||||
continue;
|
||||
}
|
||||
|
||||
// handle markdown
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import Instance from '../instance.js';
|
||||
import Emoji from '../emoji.js';
|
||||
|
||||
let user_cache = Object;
|
||||
import { parseText as parseEmojis } from '../emoji.js';
|
||||
|
||||
export default class User {
|
||||
id;
|
||||
|
@ -11,66 +8,6 @@ export default class User {
|
|||
avatar_url;
|
||||
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() {
|
||||
return this.nickname || this.username;
|
||||
}
|
||||
|
@ -80,4 +17,9 @@ export default class User {
|
|||
if (this.host) res += "@" + this.host;
|
||||
return res;
|
||||
}
|
||||
|
||||
get rich_name() {
|
||||
if (!this.nickname) return this.username;
|
||||
return parseEmojis(this.nickname, this.host);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue