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
|
**/.DS_Store
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
# .secret/
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "spacesocial-client",
|
"name": "spacesocial-client",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "spacesocial-client",
|
"name": "spacesocial-client",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||||
|
|
|
@ -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) +
|
||||||
|
parseText(text.substring(index + length + 1), 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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">
|
||||||
|
|
108
src/post/post.js
108
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,9 +70,8 @@ export default class Post {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Instance.get_instance().type !== Instance.types.MASTODON) {
|
|
||||||
// handle links
|
// 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
|
// 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);
|
||||||
|
@ -148,21 +81,28 @@ export default class Post {
|
||||||
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…
Reference in a new issue