From 960614ec6fca5d0e2f7aca5b0ca2f8fba94ed905 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 12 Oct 2023 20:13:42 +0200 Subject: [PATCH] Merge patchset from akko branch --- README.md | 35 ++++-- app/javascript/flavours/glitch/api.js | 11 ++ .../compose/components/action_bar.jsx | 2 - .../compose/components/navigation_bar.jsx | 8 +- .../compose/containers/warning_container.jsx | 2 +- .../local_settings/navigation/index.jsx | 9 +- .../features/local_settings/page/index.jsx | 4 +- .../components/deprecated_settings_modal.jsx | 2 +- .../features/ui/components/link_footer.jsx | 34 +----- .../ui/components/navigation_panel.jsx | 1 - app/javascript/flavours/glitch/stream.js | 3 +- .../flavours/glitch/utils/backend_links.js | 6 +- .../flavours/glitch/utils/log_out.js | 2 +- app/javascript/mastodon/locales/en.json | 2 +- public/auth.js | 101 +++++++++++++++++ public/images/mascot.svg | 11 ++ public/index.html | 33 ++++++ public/login.html | 13 +++ public/logout.html | 14 +++ public/manifest.json | 12 ++ public/prepare.html | 11 ++ public/verify-state.js | 103 ++++++++++++++++++ 22 files changed, 350 insertions(+), 69 deletions(-) create mode 100644 public/auth.js create mode 100644 public/images/mascot.svg create mode 100644 public/index.html create mode 100644 public/login.html create mode 100644 public/logout.html create mode 100644 public/manifest.json create mode 100644 public/prepare.html create mode 100644 public/verify-state.js diff --git a/README.md b/README.md index f878752fe35c6d39e6c551f9e5d775b00199c576..79a63cb505bef28465e7819d5c1e0cc42a887cc1 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,31 @@ -# Mastodon Glitch Edition +# Mastodon Glitch Edition (standalone frontend) -> Now with automated deploys! +This is a very hacky fork of akkoma-masto-fe that adds standalone support (meaning your browser can OAuth against an arbitrary instance). It's currently tested to "work" (login doesn't break, basic functionality works) with Iceshrimp and GoToSocial (and it obviously works with Mastodon). -[![Build Status](https://img.shields.io/circleci/project/github/glitch-soc/mastodon.svg)][circleci] -[![Code Climate](https://img.shields.io/codeclimate/maintainability/glitch-soc/mastodon.svg)][code_climate] +To try this out, go to [masto-fe.iceshrimp.dev](https://masto-fe.iceshrimp.dev), type in your instance domain name (for split domain setups, use the web domain) & press the button. -[circleci]: https://circleci.com/gh/glitch-soc/mastodon -[code_climate]: https://codeclimate.com/github/glitch-soc/mastodon +To set this up yourself, clone the repo into e.g. `/home/user/masto-fe-standalone` and run `yarn && yarn build:production` (you might have to use `NODE_OPTIONS=--openssl-legacy-provider` until we've rebased this onto upstream glitch). -So here's the deal: we all work on this code, and anyone who uses that does so absolutely at their own risk. can you dig it? +Then configure nginx for a subdomain like this: -- You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/). -- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/). +``` +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + include sites/example.com/inc/ssl.conf; + server_name masto.example.com; + + location / { + root /home/user/masto-fe-standalone/public/; + index index.html; + try_files $uri /index.html; + } +} +``` + +And open `https://masto.example.com` in your browser, type in your instance domain, press the button & follow the OAuth flow. + +Should anything break, open `https://masto.example.com/logout.html` or clear local storage manually. diff --git a/app/javascript/flavours/glitch/api.js b/app/javascript/flavours/glitch/api.js index 948ffbc95c60e8c24b339d8a280b49d507f1693d..73e3d8b3716024327a6d626e5e5518b0c302fcd6 100644 --- a/app/javascript/flavours/glitch/api.js +++ b/app/javascript/flavours/glitch/api.js @@ -53,6 +53,15 @@ const authorizationHeaderFromState = getState => { /** * @param {() => import('immutable').Map} getState + * @returns string + */ +const baseUrlFromState = getState => { + const baseUrl = getState && getState().getIn(['meta', 'base_url'], ''); + return `${baseUrl}`; +}; + +/** + * @param {() => import('immutable').Map} getState * @returns {import('axios').AxiosInstance} */ export default function api(getState) { @@ -62,6 +71,8 @@ export default function api(getState) { ...authorizationHeaderFromState(getState), }, + baseURL: baseUrlFromState(getState), + transformResponse: [ function (data) { try { diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx index ff7d4d03dccdafa47c1f54f1456e281296449895..3c971a7c0cd8bcd88416c0157fdb8a797e28914c 100644 --- a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx @@ -42,8 +42,6 @@ class ActionBar extends PureComponent { let menu = []; - menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink }); - menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink }); menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); menu.push(null); menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx index 383a9db52845f214812c9b050abf67fc2a2cda9f..988dfd30c2fd2ef38e2ee4b4f81eb97ea85bd71e 100644 --- a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx @@ -27,16 +27,10 @@ export default class NavigationBar extends ImmutablePureComponent {
+
{this.props.account.get('display_name')}
@{this.props.account.get('acct')} - - { profileLink !== undefined && ( - - )}
diff --git a/app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx b/app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx index 16916ba9c037915cc66738d00e72a0c265d8d52b..16a235852996e0ddbf04eb62ef2c668ee683ca13 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx +++ b/app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx @@ -19,7 +19,7 @@ const mapStateToProps = state => ({ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => { if (needsLockWarning) { - return }} />} />; + return }} />} />; } if (hashtagWarning) { diff --git a/app/javascript/flavours/glitch/features/local_settings/navigation/index.jsx b/app/javascript/flavours/glitch/features/local_settings/navigation/index.jsx index 022d817126344a6afbc39df343da6ab2edcfce11..39894be4003f0f65ef649899d9033f2ecf50fabf 100644 --- a/app/javascript/flavours/glitch/features/local_settings/navigation/index.jsx +++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.jsx @@ -73,15 +73,8 @@ class LocalSettingsNavigation extends PureComponent { /> - + - + ), }} /> diff --git a/app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.jsx index ba77feb6a59d0023127e76ffc4df986f15765ea1..e3728bc173c930a7bf06f41bc58b7829fe004f7f 100644 --- a/app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.jsx @@ -68,7 +68,7 @@ class DeprecatedSettingsModal extends PureComponent {
    { settings.map((setting_name) => (
  • - +
  • )) }
diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx b/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx index 0ef37bb2391d74bf1cc3b5580ed18105645dc181..8343de711bf670f2ded2e70730c2a3ddb6400ff5 100644 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx @@ -64,42 +64,12 @@ class LinkFooter extends PureComponent { return (

- {domain}: - {' '} - - {statusPageUrl && ( - <> - {DividingCircle} - - - )} - {canInvite && ( - <> - {DividingCircle} - - - )} - {canProfileDirectory && ( - <> - {DividingCircle} - - - )} + Masto-FE-standalone {DividingCircle} - -

- -

- Mastodon: - {' '} - - {DividingCircle} - + {DividingCircle} {DividingCircle} - - {DividingCircle} v{version}

diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx index f6984d5adbf2fd409034b7362da01170245179d5..38c8c153118d35647264f79adcd8f60d3cd9b94a 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx @@ -104,7 +104,6 @@ class NavigationPanel extends Component {
- {!!preferencesLink && } )} diff --git a/app/javascript/flavours/glitch/stream.js b/app/javascript/flavours/glitch/stream.js index 55f009e130307f625170eb70dbc77514c641eaa4..554889bc3de154cd3f6bc060d45eeb2922b07b5d 100644 --- a/app/javascript/flavours/glitch/stream.js +++ b/app/javascript/flavours/glitch/stream.js @@ -235,8 +235,9 @@ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { conne channelName = params.shift(); if (streamingAPIBaseURL.startsWith('ws')) { + params.push(`access_token=${accessToken}`); // @ts-expect-error - const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken); + const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming?${params.join('&')}`, accessToken); // @ts-expect-error ws.onopen = connected; diff --git a/app/javascript/flavours/glitch/utils/backend_links.js b/app/javascript/flavours/glitch/utils/backend_links.js index 2028a1e60852b75ce3f8648371306ac2cca08fc1..fc2005290746bc765b85fdb28fba389c5a99c3e3 100644 --- a/app/javascript/flavours/glitch/utils/backend_links.js +++ b/app/javascript/flavours/glitch/utils/backend_links.js @@ -1,6 +1,6 @@ -export const preferencesLink = '/settings/preferences'; -export const profileLink = '/settings/profile'; -export const signOutLink = '/auth/sign_out'; +export const preferencesLink = undefined; +export const profileLink = undefined; +export const signOutLink = '/logout.html'; export const privacyPolicyLink = '/privacy-policy'; export const accountAdminLink = (id) => `/admin/accounts/${id}`; export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`; diff --git a/app/javascript/flavours/glitch/utils/log_out.js b/app/javascript/flavours/glitch/utils/log_out.js index a7c7ef54549267525401a7b6d22c74902a9c7a2f..8c604e102fbe745c033d0a25200949015cf90b0e 100644 --- a/app/javascript/flavours/glitch/utils/log_out.js +++ b/app/javascript/flavours/glitch/utils/log_out.js @@ -26,7 +26,7 @@ export const logOut = () => { submitButton.setAttribute('type', 'submit'); form.appendChild(submitButton); - form.method = 'post'; + form.method = 'get'; form.action = signOutLink; form.style.display = 'none'; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 4399b99951336321c3b51731f8ed4177870cdbab..68f66ba9c3279b4e09c09e7b8a4ad7c760857b51 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -283,7 +283,7 @@ "footer.invite": "Invite people", "footer.keyboard_shortcuts": "Keyboard shortcuts", "footer.privacy_policy": "Privacy policy", - "footer.source_code": "View source code", + "footer.source_code": "Source code", "footer.status": "Status", "generic.saved": "Saved", "getting_started.heading": "Getting started", diff --git a/public/auth.js b/public/auth.js new file mode 100644 index 0000000000000000000000000000000000000000..6066b6896272a5fee2f0ae1058a2daee867455c0 --- /dev/null +++ b/public/auth.js @@ -0,0 +1,101 @@ +document.addEventListener("DOMContentLoaded", async function() { + await ready(); +}); + +async function ready() { + const domain = localStorage.getItem('domain'); + let accessToken = localStorage.getItem(`access_token`); + + if (domain) document.getElementById('instance').value = domain; + + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (domain && code && !accessToken) await getToken(code, domain).then(res => accessToken = res); + if (accessToken) { + window.location.href = '/prepare.html'; + } +} + +async function auth() { + setMessage('Please wait'); + const instance = document.getElementById('instance').value; + const domain = instance.match(/(?:https?:\/\/)?(.*)/)[1]; + if (!domain) { + setMessage('Invalid instance', false); + return; + } + + localStorage.setItem('domain', domain); + + // We need to run this every time in cases like Iceshrimp, where the client id/secret aren't reusable (yet) because they contain use-once session information + await registerApp(domain); + + authorize(domain); +} + +async function registerApp(domain) { + setMessage('Registering app'); + + const appsUrl = `https://${domain}/api/v1/apps`; + const formData = new FormData(); + formData.append('client_name', 'Masto-FE standalone'); + formData.append('redirect_uris', document.location.origin + document.location.pathname); + formData.append('scopes', 'read write follow push'); + + // eslint-disable-next-line promise/catch-or-return + await fetch(appsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams(formData), + }) + .then(async res => { + const app = await res.json(); + localStorage.setItem(`client_id`, app.client_id); + localStorage.setItem(`client_secret`, app.client_secret); + }); +} + +function authorize(domain) { + setMessage('Authorizing'); + const clientId = localStorage.getItem(`client_id`); + document.location.href = `https://${domain}/oauth/authorize?response_type=code&client_id=${clientId}&redirect_uri=${document.location.origin + document.location.pathname}&scope=read+write+follow+push`; +} + +async function getToken(code, domain) { + setMessage('Getting token'); + + const tokenUrl = `https://${domain}/oauth/token`; + const clientId = localStorage.getItem(`client_id`); + const clientSecret = localStorage.getItem(`client_secret`); + + const formData = new FormData(); + formData.append('grant_type', 'authorization_code'); + formData.append('code', code); + formData.append('client_id', clientId); + formData.append('client_secret', clientSecret); + formData.append('scope', 'read write follow push'); + formData.append('redirect_uri', document.location.origin + document.location.pathname); + + + // eslint-disable-next-line promise/catch-or-return + return fetch(tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams(formData), + }) + .then(async res => { + const app = await res.json(); + if (app.access_token) localStorage.setItem(`access_token`, app.access_token); + return app.access_token; + }); +} + +function setMessage(message, disabled = true) { + document.getElementById('message').textContent = message; + document.getElementById('btn').disabled = disabled; +} \ No newline at end of file diff --git a/public/images/mascot.svg b/public/images/mascot.svg new file mode 100644 index 0000000000000000000000000000000000000000..23384b66173e09667287111efc38dc5030d4d870 --- /dev/null +++ b/public/images/mascot.svg @@ -0,0 +1,11 @@ + +image/svg+xml + + + + + + + + + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000000000000000000000000000000000000..b9f329c6a0418ec72361553c3583596156bc8fd8 --- /dev/null +++ b/public/index.html @@ -0,0 +1,33 @@ + + + + + + Masto-FE standalone + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + diff --git a/public/login.html b/public/login.html new file mode 100644 index 0000000000000000000000000000000000000000..90e56024af644f2870614f449f26ef72a19e67c4 --- /dev/null +++ b/public/login.html @@ -0,0 +1,13 @@ + + + + + Login | Masto-FE standalone + + + + + + + + \ No newline at end of file diff --git a/public/logout.html b/public/logout.html new file mode 100644 index 0000000000000000000000000000000000000000..f49e3dc501847a9460381206d1df769f59b75e02 --- /dev/null +++ b/public/logout.html @@ -0,0 +1,14 @@ + + + + + Logout | Masto-FE standalone + + + +Clearing local storage and redirecting back to login... + + \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..c1c0f9dda7c20cf48aa349a288b3c9c5f6e6910d --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,12 @@ +{ + "background_color": "#191b22", + "categories": ["social"], + "description": "Masto-FE standalone", + "display": "standalone", + "name": "Masto-FE standalone", + "serviceworker": { + "src": "/sw.js" + }, + "start_url": "/getting-started", + "theme_color": "#282c37" +} diff --git a/public/prepare.html b/public/prepare.html new file mode 100644 index 0000000000000000000000000000000000000000..e6e5f1e0cc923d5d85cdb28bfcc7de97d60b00e8 --- /dev/null +++ b/public/prepare.html @@ -0,0 +1,11 @@ + + + + + Login | Masto-FE standalone + + + +

Preparing state object...

+ + \ No newline at end of file diff --git a/public/verify-state.js b/public/verify-state.js new file mode 100644 index 0000000000000000000000000000000000000000..5308ff6f46fcfdcf18cf1733153a035cbfddb902 --- /dev/null +++ b/public/verify-state.js @@ -0,0 +1,103 @@ +loadState().then(_ => null); + +async function loadState() { + const domain = localStorage.getItem('domain'); + const access_token = localStorage.getItem('access_token'); + const storedState = localStorage.getItem('initial_state'); + + if (!domain || !access_token) { + window.location.href = '/login.html'; + return; + } + + if (storedState && window.location.pathname !== '/prepare.html') { + document.getElementById('initial-state').textContent = storedState; + } + + const apiUrl = `https://${domain}/api`; + const instance = await fetch(`${apiUrl}/v1/instance`).then(async p => await p.json()); + const options = {headers: {Authorization: `Bearer ${access_token}`}}; + const credentials = await fetch(`${apiUrl}/v1/accounts/verify_credentials`, options).then(async p => await p.json()); + const state = { + "accounts": { + "plc":{ + "accepts_direct_messages_from":"everybody", + "acct": credentials.acct, + "avatar": credentials.avatar, + "avatar_static": credentials.avatar_static, + "bot": credentials.bot, + "created_at": credentials.created_at, + "display_name": credentials.display_name, + "emojis":[], + "fields":[], + "follow_requests_count":0, + "followers_count": credentials.followers_count, + "following_count": credentials.following_count, + "fqn":`${credentials.acct}@${domain}`, + "header": credentials.header, + "header_static": credentials.header_static, + "id": credentials.id, + "last_status_at": credentials.created_at, + "locked": credentials.locked, + "note":"", + "source": credentials.source, + "statuses_count": credentials.statuses_count, + "url": credentials.url, + "username": credentials.acct + } + }, + "char_limit": instance.configuration.statuses.max_characters, + "compose": { + "allow_content_types": [ + "text/x.misskeymarkdown" + ], + "default_privacy": credentials.source.privacy, + "default_sensitive": credentials.source.sensitive, + "me": credentials.id + }, + "media_attachments": { + "accept_content_types": instance.configuration.media_attachments.supported_mime_types + }, + "meta": { + "access_token": access_token, + "admin": "0", + "advanced_layout": true, + "auto_play_gif": false, + "boost_modal": false, + "compact_reaction": false, + "delete_modal": true, + "display_sensitive_media": false, + "domain": domain, + "enable_reaction": true, + "locale": "en", + "mascot": "/images/mascot.svg", + "max_toot_chars": instance.configuration.statuses.max_characters, + "me": credentials.id, + "reduce_motion": false, + "show_quote_button": true, + "base_url": `https://${domain}`, + "streaming_api_base_url": `wss://${domain}`, + "title": `${instance.title}`, + "unfollow_modal": true, + "source_url": 'https://iceshrimp.dev/iceshrimp/masto-fe-standalone', + "version": instance.version + }, + "poll_limits": { + "max_expiration": instance.configuration.polls.max_expiration, + "max_option_chars": instance.configuration.polls.max_characters_per_option, + "max_options": instance.configuration.polls.max_options, + "min_expiration": instance.configuration.polls.min_expiration + }, + "push_subscription": null, + "rights": { + "admin": false, + "delete_others_notice": false + }, + "settings": {} + }; + + const json = JSON.stringify(state); + if (window.location.pathname !== '/prepare.html') document.getElementById('initial-state').textContent = json; + localStorage.setItem("initial_state", json); + if (window.location.pathname === '/prepare.html') window.location.href = '/'; +} \ No newline at end of file