huge refactor. addresses #21 w/ inifinite scrolling notifications
This commit is contained in:
parent
f883b61659
commit
2e64f63caa
29 changed files with 887 additions and 1030 deletions
|
@ -1,339 +0,0 @@
|
|||
import { client } from '$lib/client/client.js';
|
||||
import { user } from '$lib/stores/user.js';
|
||||
import { capabilities } from '../client/instance.js';
|
||||
import Post from '$lib/post.js';
|
||||
import User from '$lib/user/user.js';
|
||||
import Emoji from '$lib/emoji.js';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function createApp(host) {
|
||||
let form = new FormData();
|
||||
form.append("client_name", "Campfire");
|
||||
form.append("redirect_uris", `${location.origin}/callback`);
|
||||
form.append("scopes", "read write push");
|
||||
form.append("website", "https://campfire.bliss.town");
|
||||
|
||||
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() {
|
||||
return `https://${get(client).instance.host}/oauth/authorize` +
|
||||
`?client_id=${get(client).app.id}` +
|
||||
"&scope=read+write+push" +
|
||||
`&redirect_uri=${location.origin}/callback` +
|
||||
"&response_type=code";
|
||||
}
|
||||
|
||||
export async function getToken(code) {
|
||||
let form = new FormData();
|
||||
form.append("client_id", get(client).app.id);
|
||||
form.append("client_secret", get(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://${get(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 form = new FormData();
|
||||
form.append("client_id", get(client).app.id);
|
||||
form.append("client_secret", get(client).app.secret);
|
||||
form.append("token", get(client).app.token);
|
||||
|
||||
const res = await fetch(`https://${get(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 verifyCredentials() {
|
||||
let url = `https://${get(client).instance.host}/api/v1/accounts/verify_credentials`;
|
||||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getNotifications(since_id, limit, types) {
|
||||
if (!get(user)) return false;
|
||||
|
||||
let url = `https://${get(client).instance.host}/api/v1/notifications`;
|
||||
|
||||
let params = new URLSearchParams();
|
||||
if (since_id) params.append("since_id", since_id);
|
||||
if (limit) params.append("limit", limit);
|
||||
if (types) params.append("types", types.join(','));
|
||||
const params_string = params.toString();
|
||||
if (params_string) url += '?' + params_string;
|
||||
|
||||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getTimeline(last_post_id) {
|
||||
if (!get(user)) return false;
|
||||
let url = `https://${get(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 " + get(client).app.token }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getPost(post_id, ancestor_count) {
|
||||
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}`;
|
||||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => { return res.ok ? res.json() : false });
|
||||
|
||||
if (data === false) return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getPostContext(post_id) {
|
||||
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/context`;
|
||||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => { return res.ok ? res.json() : false });
|
||||
|
||||
if (data === false) return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function boostPost(post_id) {
|
||||
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/reblog`;
|
||||
const data = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => { return res.ok ? res.json() : false });
|
||||
|
||||
if (data === false) return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function unboostPost(post_id) {
|
||||
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreblog`;
|
||||
const data = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => { return res.ok ? res.json() : false });
|
||||
|
||||
if (data === false) return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function favouritePost(post_id) {
|
||||
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/favourite`;
|
||||
const data = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => { return res.ok ? res.json() : false });
|
||||
|
||||
if (data === false) return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function unfavouritePost(post_id) {
|
||||
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unfavourite`;
|
||||
const data = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => { return res.ok ? res.json() : false });
|
||||
|
||||
if (data === false) return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function reactPost(post_id, shortcode) {
|
||||
// for whatever reason (at least in my testing on iceshrimp)
|
||||
// using shortcodes for external emoji results in a fallback
|
||||
// to the default like emote.
|
||||
// identical api calls on chuckya instances do not display
|
||||
// this behaviour.
|
||||
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/react/${encodeURIComponent(shortcode)}`;
|
||||
const data = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => { return res.ok ? res.json() : false });
|
||||
|
||||
if (data === false) return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function unreactPost(post_id, shortcode) {
|
||||
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`;
|
||||
const data = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => { return res.ok ? res.json() : false });
|
||||
|
||||
if (data === false) return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function parsePost(data, ancestor_count) {
|
||||
let post = new Post();
|
||||
|
||||
post.text = data.content;
|
||||
post.html = data.content;
|
||||
|
||||
post.reply = null;
|
||||
if ((data.in_reply_to_id || data.reply) &&
|
||||
ancestor_count !== 0
|
||||
) {
|
||||
const reply_data = data.reply || await getPost(data.in_reply_to_id, ancestor_count - 1);
|
||||
// if the post returns false, we probably don't have permission to read it.
|
||||
// we'll respect the thread's privacy, and leave it alone :)
|
||||
if (!reply_data) return false;
|
||||
post.reply = await parsePost(reply_data, ancestor_count - 1, false);
|
||||
}
|
||||
|
||||
post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null;
|
||||
|
||||
post.id = data.id;
|
||||
post.created_at = new Date(data.created_at);
|
||||
post.user = await parseUser(data.account);
|
||||
post.warning = data.spoiler_text;
|
||||
post.boost_count = data.reblogs_count;
|
||||
post.reply_count = data.replies_count;
|
||||
post.favourite_count = data.favourites_count;
|
||||
post.favourited = data.favourited;
|
||||
post.boosted = data.reblogged;
|
||||
post.mentions = data.mentions;
|
||||
post.files = data.media_attachments;
|
||||
post.url = data.url;
|
||||
post.visibility = data.visibility;
|
||||
|
||||
post.emojis = [];
|
||||
if (data.emojis) {
|
||||
data.emojis.forEach(emoji_data => {
|
||||
let name = emoji_data.shortcode.split('@')[0];
|
||||
post.emojis.push(parseEmoji({
|
||||
id: name + '@' + post.user.host,
|
||||
name: name,
|
||||
host: post.user.host,
|
||||
url: emoji_data.url,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
if (data.reactions && get(client).instance.capabilities.includes(capabilities.REACTIONS)) {
|
||||
post.reactions = parseReactions(data.reactions);
|
||||
}
|
||||
return post;
|
||||
}
|
||||
|
||||
export async function parseUser(data) {
|
||||
if (!data) {
|
||||
console.error("Attempted to parse user data but no data was provided");
|
||||
return null;
|
||||
}
|
||||
let user = await get(client).getCacheUser(data.id);
|
||||
|
||||
if (user) return user;
|
||||
// cache miss!
|
||||
|
||||
user = new User();
|
||||
user.id = data.id;
|
||||
user.nickname = data.display_name.trim();
|
||||
user.username = data.username;
|
||||
user.avatar_url = data.avatar;
|
||||
user.url = data.url;
|
||||
|
||||
if (data.acct.includes('@'))
|
||||
user.host = data.acct.split('@')[1];
|
||||
else
|
||||
user.host = get(client).instance.host;
|
||||
|
||||
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(parseEmoji(emoji_data));
|
||||
});
|
||||
|
||||
get(client).putCacheUser(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
export function parseReactions(data) {
|
||||
let reactions = [];
|
||||
data.forEach(reaction_data => {
|
||||
let reaction = {
|
||||
count: reaction_data.count,
|
||||
name: reaction_data.name,
|
||||
me: reaction_data.me,
|
||||
};
|
||||
if (reaction_data.url) reaction.url = reaction_data.url;
|
||||
reactions.push(reaction);
|
||||
});
|
||||
return reactions;
|
||||
}
|
||||
|
||||
export function parseEmoji(data) {
|
||||
let emoji = new Emoji(
|
||||
data.id,
|
||||
data.name,
|
||||
data.host,
|
||||
data.url,
|
||||
);
|
||||
get(client).putCacheEmoji(emoji);
|
||||
return emoji;
|
||||
}
|
||||
|
||||
export async function getUser(user_id) {
|
||||
let url = `https://${get(client).instance.host}/api/v1/accounts/${user_id}`;
|
||||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": "Bearer " + get(client).app.token }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
34
src/lib/client/app.js
Normal file
34
src/lib/client/app.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import { app_name } from '$lib/config.js';
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
// if app is falsy, assume user has not begun the login process.
|
||||
// if app.token is falsy, assume user has not logged in.
|
||||
export const app = writable(loadApp());
|
||||
|
||||
// write to localStorage on each update
|
||||
app.subscribe(app => {
|
||||
saveApp(app);
|
||||
});
|
||||
|
||||
/**
|
||||
* Saves the provided app to localStorage.
|
||||
* If `app` is falsy, data is removed from localStorage.
|
||||
* @param {Object} app
|
||||
*/
|
||||
function saveApp(app) {
|
||||
if (!browser) return;
|
||||
if (!app) localStorage.removeItem(app_name + "_app");
|
||||
localStorage.setItem(app_name + "_app", JSON.stringify(app));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns application data loaded from localStorage, if it exists.
|
||||
* Otherwise, returns false.
|
||||
*/
|
||||
function loadApp() {
|
||||
if (!browser) return;
|
||||
let data = localStorage.getItem(app_name + "_app");
|
||||
if (!data) return false;
|
||||
return JSON.parse(data);
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
import { Instance, server_types } from './instance.js';
|
||||
import * as api from './api.js';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { last_read_notif_id } from '$lib/notifications.js';
|
||||
import { user, logged_in } from '$lib/stores/user.js';
|
||||
|
||||
export const client = writable(false);
|
||||
|
||||
const save_name = "campfire";
|
||||
|
||||
export class Client {
|
||||
instance;
|
||||
app;
|
||||
#cache;
|
||||
|
||||
constructor() {
|
||||
this.instance = null;
|
||||
this.app = null;
|
||||
this.cache = {
|
||||
users: {},
|
||||
emojis: {},
|
||||
};
|
||||
}
|
||||
|
||||
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()).catch(error => { console.error(error) });
|
||||
if (!data) {
|
||||
console.error(`Failed to connect to ${host}`);
|
||||
return `Failed to connect to ${host}!`;
|
||||
}
|
||||
|
||||
this.instance = new Instance(host, data.version);
|
||||
if (this.instance.type == server_types.UNSUPPORTED) {
|
||||
console.warn(`Server ${host} is unsupported - ${data.version}`);
|
||||
if (!confirm(
|
||||
`This app does not officially support ${host}. ` +
|
||||
`Things may break, or otherwise not work as epxected! ` +
|
||||
`Are you sure you wish to continue?`
|
||||
)) return false;
|
||||
} else {
|
||||
console.log(`Server is "${this.instance.type}" (or compatible) with capabilities: [${this.instance.capabilities}].`);
|
||||
}
|
||||
|
||||
this.app = await api.createApp(host);
|
||||
|
||||
if (!this.app || !this.instance) {
|
||||
console.error("Failed to create app. Check the network logs for details.");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.save();
|
||||
|
||||
client.set(this);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getOAuthUrl() {
|
||||
return api.getOAuthUrl(this.app.secret);
|
||||
}
|
||||
|
||||
async getToken(code) {
|
||||
const token = await api.getToken(code);
|
||||
if (!token) {
|
||||
console.error("Failed to obtain access token");
|
||||
return false;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
async revokeToken() {
|
||||
return await api.revokeToken();
|
||||
}
|
||||
|
||||
async getNotifications(since_id, limit, types) {
|
||||
return await api.getNotifications(since_id, limit, types);
|
||||
}
|
||||
|
||||
async getTimeline(last_post_id) {
|
||||
return await api.getTimeline(last_post_id);
|
||||
}
|
||||
|
||||
async getPost(post_id, parent_replies, child_replies) {
|
||||
return await api.getPost(post_id, parent_replies, child_replies);
|
||||
}
|
||||
|
||||
async getPostContext(post_id) {
|
||||
return await api.getPostContext(post_id);
|
||||
}
|
||||
|
||||
async boostPost(post_id) {
|
||||
return await api.boostPost(post_id);
|
||||
}
|
||||
|
||||
async unboostPost(post_id) {
|
||||
return await api.unboostPost(post_id);
|
||||
}
|
||||
|
||||
async favouritePost(post_id) {
|
||||
return await api.favouritePost(post_id);
|
||||
}
|
||||
|
||||
async unfavouritePost(post_id) {
|
||||
return await api.unfavouritePost(post_id);
|
||||
}
|
||||
|
||||
async reactPost(post_id, shortcode) {
|
||||
return await api.reactPost(post_id, shortcode);
|
||||
}
|
||||
|
||||
async unreactPost(post_id, shortcode) {
|
||||
return await api.unreactPost(post_id, shortcode);
|
||||
}
|
||||
|
||||
putCacheUser(user) {
|
||||
this.cache.users[user.id] = user;
|
||||
client.set(this);
|
||||
}
|
||||
|
||||
async getCacheUser(user_id) {
|
||||
let user = this.cache.users[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;
|
||||
client.set(this);
|
||||
}
|
||||
|
||||
getEmoji(emoji_id) {
|
||||
let emoji = this.cache.emojis[emoji_id];
|
||||
if (!emoji) return false;
|
||||
return emoji;
|
||||
}
|
||||
|
||||
async getUser(user_id) {
|
||||
return await api.getUser(user_id);
|
||||
}
|
||||
|
||||
save() {
|
||||
if (typeof localStorage === typeof undefined) return;
|
||||
localStorage.setItem(save_name, JSON.stringify({
|
||||
version: APP_VERSION,
|
||||
instance: {
|
||||
host: this.instance.host,
|
||||
version: this.instance.version,
|
||||
},
|
||||
last_read_notif_id: get(last_read_notif_id),
|
||||
app: this.app,
|
||||
}));
|
||||
}
|
||||
|
||||
load() {
|
||||
if (typeof localStorage === typeof undefined) return;
|
||||
let json = localStorage.getItem(save_name);
|
||||
if (!json) return false;
|
||||
let saved = JSON.parse(json);
|
||||
if (!saved.version || saved.version !== APP_VERSION) {
|
||||
localStorage.removeItem(save_name);
|
||||
return false;
|
||||
}
|
||||
this.instance = new Instance(saved.instance.host, saved.instance.version);
|
||||
last_read_notif_id.set(saved.last_read_notif_id || 0);
|
||||
this.app = saved.app;
|
||||
client.set(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
async logout() {
|
||||
if (!this.instance || !this.app) return;
|
||||
if (!await this.revokeToken()) {
|
||||
console.warn("Failed to log out correctly; ditching the old tokens anyways.");
|
||||
}
|
||||
localStorage.removeItem(save_name);
|
||||
logged_in.set(false);
|
||||
client.set(new Client());
|
||||
console.log("Logged out successfully.");
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
export const server_types = {
|
||||
UNSUPPORTED: "unsupported",
|
||||
MASTODON: "mastodon",
|
||||
GLITCHSOC: "glitchsoc",
|
||||
CHUCKYA: "chuckya",
|
||||
FIREFISH: "firefish",
|
||||
ICESHRIMP: "iceshrimp",
|
||||
SHARKEY: "sharkey",
|
||||
};
|
||||
|
||||
export const capabilities = {
|
||||
MARKDOWN_CONTENT: "mdcontent",
|
||||
REACTIONS: "reactions",
|
||||
};
|
||||
|
||||
export class Instance {
|
||||
host;
|
||||
version;
|
||||
capabilities;
|
||||
type = server_types.UNSUPPORTED;
|
||||
|
||||
constructor(host, version) {
|
||||
this.host = host;
|
||||
this.version = version;
|
||||
this.#setType(version);
|
||||
this.capabilities = this.#getCapabilities(this.type);
|
||||
}
|
||||
|
||||
#setType(version) {
|
||||
this.type = server_types.UNSUPPORTED;
|
||||
if (version.constructor !== String) return;
|
||||
let version_lower = version.toLowerCase();
|
||||
for (let i = 1; i < Object.keys(server_types).length; i++) {
|
||||
const check_type = Object.values(server_types)[i];
|
||||
if (version_lower.includes(check_type)) {
|
||||
this.type = check_type;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#getCapabilities(type) {
|
||||
let c = [];
|
||||
switch (type) {
|
||||
case server_types.MASTODON:
|
||||
break;
|
||||
case server_types.GLITCHSOC:
|
||||
c.push(capabilities.REACTIONS);
|
||||
break;
|
||||
case server_types.CHUCKYA:
|
||||
c.push(capabilities.REACTIONS);
|
||||
break;
|
||||
case server_types.FIREFISH:
|
||||
c.push(capabilities.REACTIONS);
|
||||
break;
|
||||
case server_types.ICESHRIMP:
|
||||
// more trouble than it's worth atm
|
||||
// the server already hands this to us ;p
|
||||
//c.push(capabilities.MARKDOWN_CONTENT);
|
||||
c.push(capabilities.REACTIONS);
|
||||
break;
|
||||
case server_types.SHARKEY:
|
||||
c.push(capabilities.REACTIONS);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
}
|
138
src/lib/client/server.js
Normal file
138
src/lib/client/server.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
import * as api from '$lib/api.js';
|
||||
import { writable } from 'svelte/store';
|
||||
import { app_name } from '$lib/config.js';
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
const server_types = {
|
||||
UNSUPPORTED: "unsupported",
|
||||
MASTODON: "mastodon",
|
||||
GLITCHSOC: "glitchsoc",
|
||||
CHUCKYA: "chuckya",
|
||||
FIREFISH: "firefish",
|
||||
ICESHRIMP: "iceshrimp",
|
||||
SHARKEY: "sharkey",
|
||||
AKKOMA: "akkoma", // TODO: verify
|
||||
PLEROMA: "pleroma", // TODO: verify
|
||||
};
|
||||
|
||||
export const capabilities = {
|
||||
MARKDOWN_CONTENT: "mdcontent",
|
||||
REACTIONS: "reactions",
|
||||
};
|
||||
|
||||
// if server is falsy, assume user has not begun the login process.
|
||||
export let server = writable(loadServer());
|
||||
|
||||
// write to localStorage on each update
|
||||
server.subscribe(server => {
|
||||
saveServer(server);
|
||||
});
|
||||
|
||||
/**
|
||||
* Attempts to create an server object using a given hostname.
|
||||
* @param {string} host - The domain of the target server.
|
||||
*/
|
||||
export async function createServer(host) {
|
||||
if (!host) {
|
||||
console.error("Attempted to create server without providing a hostname");
|
||||
return false;
|
||||
}
|
||||
if (host.startsWith("http://")) {
|
||||
console.error("Cowardly refusing to connect to an insecure server");
|
||||
return false;
|
||||
}
|
||||
|
||||
let server = {};
|
||||
server.host = host;
|
||||
|
||||
if (host.startsWith("https://")) host = host.substring(8);
|
||||
const data = await api.getInstance(host);
|
||||
if (!data) {
|
||||
console.error(`Failed to connect to ${host}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
server.version = data.version;
|
||||
server.type = getType(server.version);
|
||||
server.capabilities = getCapabilities(server.type);
|
||||
|
||||
if (server.type === server_types.UNSUPPORTED) {
|
||||
console.warn(`Server ${host} is unsupported (${server.version}). Things may break, or not work as expected`);
|
||||
} else {
|
||||
console.log(`Server detected as "${server.type}" (${server.version}) with capabilities: {${server.capabilities.join(', ')}}`);
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided server to localStorage.
|
||||
* If `server` is falsy, data is removed from localStorage.
|
||||
* @param {Object} server
|
||||
*/
|
||||
function saveServer(server) {
|
||||
if (!browser) return;
|
||||
if (!server) localStorage.removeItem(app_name + "_server");
|
||||
localStorage.setItem(app_name + "_server", JSON.stringify(server));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns server data loaded from localStorage, if it exists.
|
||||
* Otherwise, returns false.
|
||||
*/
|
||||
function loadServer() {
|
||||
if (!browser) return;
|
||||
let data = localStorage.getItem(app_name + "_server");
|
||||
if (!data) return false;
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of an server, inferred from its version string.
|
||||
* @param {string} version
|
||||
* @returns the inferred server_type
|
||||
*/
|
||||
function getType(version) {
|
||||
if (version.constructor !== String) return;
|
||||
let version_lower = version.toLowerCase();
|
||||
for (let i = 1; i < Object.keys(server_types).length; i++) {
|
||||
const type = Object.values(server_types)[i];
|
||||
if (version_lower.includes(type)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return server_types.UNSUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of capabilities for a given server_type.
|
||||
* @param {string} type
|
||||
*/
|
||||
function getCapabilities(type) {
|
||||
let c = [];
|
||||
switch (type) {
|
||||
case server_types.MASTODON:
|
||||
break;
|
||||
case server_types.GLITCHSOC:
|
||||
c.push(capabilities.REACTIONS);
|
||||
break;
|
||||
case server_types.CHUCKYA:
|
||||
c.push(capabilities.REACTIONS);
|
||||
break;
|
||||
case server_types.FIREFISH:
|
||||
c.push(capabilities.REACTIONS);
|
||||
break;
|
||||
case server_types.ICESHRIMP:
|
||||
// more trouble than it's worth atm
|
||||
// mastodon API already hands html to us
|
||||
//c.push(capabilities.MARKDOWN_CONTENT);
|
||||
c.push(capabilities.REACTIONS);
|
||||
break;
|
||||
case server_types.SHARKEY:
|
||||
c.push(capabilities.REACTIONS);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return c;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue