123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- //////////////////////////////////////////////////////////////////////////////
- /// @file main.js
- /// @author SENOO, Ken
- /// @copyright CC0
- //////////////////////////////////////////////////////////////////////////////
- "use strict";
- document.querySelector('#button').onclick = async function() {
- let input = new URL(document.querySelector('#input').value);
- const id = input.pathname.replace(/\/+$/, '').replace(/.*\//, '');
- const handle = document.querySelector('#handle').value;
- const password = document.querySelector('#password').value;
- let post;
- do {
- const funcs = [getMisskey, getMastodon, getTwitter, getATProtocol];
- for (const key in funcs) {
- if (post = await funcs[key](input, id, handle, password)) {
- break;
- }
- }
- } while (false);
- format(post);
- return;
- function removeScheme(url) {
- return url.replace(/^[^:]*:/, '');
- }
- function format(post) {
- const bq = document.createElement('blockquote');
- bq.setAttribute('style', `background:${post.color};background-image:`
- + 'linear-gradient(hsla(0,0%,100%,0.5),hsla(0,0%,100%,0.5))');
- function createPost(post) {
- const article = document.createElement('article');
- const header = document.createElement('header');
- header.setAttribute('style', 'display:flex;line-height:1.5');
- const img = document.createElement('img');
- img.setAttribute('referrerpolicy', 'no-referrer');
- img.setAttribute('alt', 'avatar');
- img.setAttribute('src', removeScheme(post.avatar));
- img.setAttribute('style', 'height:4.5em');
- header.appendChild(img);
- let profile = `<div><a href="${post.from.url}">${post.date}</a></div><div
- >${htmlspecialchars(post.from.display_name)}|<a
- href="${post.from.profile_url}">${post.from.acct}</a></div>`;
- if (post.to) {
- profile += `<div><a href="${post.to.url}">replying to</a>
- ${htmlspecialchars(post.to.display_name)}|<a
- href="${post.to.profile_url}">${post.to.acct}</a></div>`;
- }
- header.innerHTML +=
- `<div>${profile.replace(/ /g, ' ')}</div>`;
- article.appendChild(header);
- const main = document.createElement('main');
- if (post.html) {
- // Mastodon returns invalid XML .
- main.innerHTML = post.html.replace(/ /g, ' ');
- } else if (post.text) {
- const span = document.createElement('span');
- span.textContent = post.text;
- span.setAttribute('style', 'white-space:pre-wrap;word-wrap:word-break');
- main.appendChild(span);
- }
- if (post.cw) {
- main.innerHTML = `<p>${post.cw}</p>${main.innerHTML}`;
- }
- article.appendChild(main);
- const footer = document.createElement('footer');
- const media_types = {unknown: 'a',
- image: 'img', gifv: 'img', video: 'video', audio: 'audio'};
- for (const key in post.attachments) {
- const media = post.attachments[key];
- if (!media.tag) {
- console.log('No media.');
- continue;
- }
- const el = document.createElement(media.tag);
- el.setAttribute('referrerpolicy', 'no-referrer');
- if (media.alt) {
- el.setAttribute('alt', media.alt);
- }
- const src = media.tag === 'a' ? 'href' : 'src';
- el.setAttribute(src, removeScheme(media.url));
- if (media.tag === 'a') {
- el.textContent = 'attachment' + key;
- }
- if (media.width) {
- el.setAttribute('width', media.width);
- }
- footer.appendChild(el);
- }
- if (footer.hasChildNodes()) {
- article.appendChild(footer);
- }
- return article;
- }
- let parent = createPost(post);
- if (post.cite) {
- let child = createPost(post.cite);
- child.setAttribute('style', 'border:thin solid');
- parent.getElementsByTagName('main')[0].appendChild(child);
- }
- bq.appendChild(parent);
- // Output
- document.querySelector("#output").textContent = bq.outerHTML;
- document.querySelector("#view").innerHTML = bq.outerHTML;
- navigator.clipboard.writeText(bq.outerHTML);
- }
- async function getMastodon(permalink, id) {
- const gw = permalink.origin + "/api/v1/statuses/"
- const url = new URL(gw + id);
- return await fetch(url.href).then(res => res.json()).then(async data => {
- let post = {
- attachments: [],
- avatar: data.account.avatar,
- // https://joinmastodon.org/
- color: 'rgb(99,100,255)',
- // It seems remote post response <br> (not XML <br /> tag ex: https://mastodon-japan.net/@upasampada@fedibird.com/109776574601158598)
- html: data.content.replace(/\u003cbr\u003e/g, '\u003cbr /\u003e'),
- cw: data.spoiler_text,
- date: data.created_at,
- from: {
- url: data.url,
- acct: data.account.acct +
- (data.account.acct.includes('@') ? '' : '@'+url.host),
- display_name: data.account.display_name,
- profile_url: data.account.url,
- },
- };
- if (data.in_reply_to_id) {
- post.to = await fetch(gw+data.in_reply_to_id)
- .then(res => res.json()).then(data => { return {
- url: data.url,
- acct: data.account.acct +
- (data.account.acct.includes('@') ? '' : '@'+url.host),
- display_name: data.account.display_name
- ? data.account.display_name : data.account.username,
- profile_url: data.account.url,
- }}).catch(e => console.log(e));
- }
- const media_types = {unknown: 'a',
- image: 'img', gifv: 'img', video: 'video', audio: 'audio'};
- for (const key in data.media_attachments) {
- const media = data.media_attachments[key];
- const attachment = {
- tag: media_types[media.type],
- url: media.url,
- };
- if (media.meta) {
- if (media.meta.description) {
- attachment.alt = media.meta.description;
- }
- if (media.meta.small) {
- attachment.width = media.meta.small.width;
- }
- }
- post.attachments.push(attachment);
- }
- return post;
- }).catch(e => console.log(e));
- }
- async function getMisskey(permalink, id) {
- const gw = new URL(permalink.origin + '/api/notes/show');
- return await fetch(gw.href, {method: 'POST', body: `{"noteId": "${id}"}`,
- headers: {'Content-Type': 'application/json'}}
- ).then(res => res.json()).then(async data => {
- function getMP(data) {
- let host = data.user.host ? data.user.host : gw.host;
- let post = {
- attachments: [],
- avatar: data.user.avatarUrl,
- // https://github.com/misskey-dev/misskey/blob/develop/packages/backend/assets/icons/192.png
- color: 'rgb(158,194,63)',
- text: data.text,
- cw: data.cw,
- date: data.createdAt,
- from: {
- url: `${gw.origin}/notes/${data.id}`,
- acct: `${data.user.username}@${host}`,
- display_name: data.user.name ? data.user.name : data.user.username,
- profile_url: `${gw.origin}/@${data.user.username}@${host}`,
- },
- };
- if (data.reply) {
- const host = data.reply.user.host ? data.reply.user.host : gw.host;
- post.to = {
- url: `${gw.origin}/notes/${data.reply.id}`,
- acct: `${data.reply.user.username}@${host}`,
- display_name: data.reply.user.name
- ? data.reply.user.name : data.reply.user.username,
- profile_url: `${gw.origin}/@${data.reply.user.username}@${host}`,
- };
- }
- for (const key in data.files) {
- const media = data.files[key];
- const attachment = {url: media.url};
- if ((""+media.type).startsWith('image')) {
- attachment.tag = 'img';
- } else if ((""+media.type).startsWith('video')) {
- attachment.tag = 'video';
- } else if ((""+media.type).startsWith('audio')) {
- attachment.tag = 'audio';
- } else if (media.type) {
- attachment.tag = 'a';
- }
- if (media.name) {
- attachment.alt = media.name;
- }
- if (media.properties) {
- attachment.width = media.properties.width;
- }
- post.attachments.push(attachment);
- }
- return post;
- }
- let post = getMP(data);
- post.cite = data.renote ? getMP(data.renote) : null;
- return post;
- }).catch(e => null);
- }
- async function getTwitter(permalink, id) {
- let gw = permalink.origin + '/api/';
- if (permalink.host === 'twitter.com') {
- gw = 'https://api.twitter.com/1.1/'
- }
- const url = new URL(gw + 'statuses/show.json?id='+ id);
- return await fetch(url.href).then(res => res.json()).then(async data => {
- let post = {
- attachments: [],
- avatar: data.user.profile_image_url,
- color: 'rgb(29,155,240)',
- text: data.text,
- cw: '',
- date: (new Date(data.created_at)).toISOString(),
- from: {
- url: url.origin + '/notice/' + data.id,
- acct: `${data.user.screen_name}@${url.host}`,
- display_name: data.user.name,
- profile_url: url.origin + '/' + data.user.screen_name,
- },
- };
- if (data.in_reply_to_status_id) {
- post.to = {
- url: url.origin + '/notice/' + data.in_reply_to_status_id,
- acct: data.in_reply_to_screen_name + '@' + url.host,
- profile_url: url.origin + '/' + data.in_reply_to_screen_name,
- };
- post.to.display_name = await fetch(
- gw+'users/show.json?screen_name='+data.in_reply_to_screen_name)
- .then(res => res.json()).then(data => data.name)
- // It seems GNU social does not support remote user.
- // And return fail back for deleted user.
- .catch(e => data.in_reply_to_screen_name);
- }
- return post;
- }).catch(e => null);
- }
- async function getATProtocol(permalink, id, handle, password) {
- const gw = 'https://bsky.social/xrpc/'
- const jwt = await fetch(gw + 'com.atproto.server.createSession',
- {method: 'POST', headers: {'Content-Type': 'application/json'},
- body: `{"identifier":"${handle}", "password":"${password}"}`})
- .then(res => res.json()).then(async data => data.accessJwt)
- .catch(e => console.log(e));
- const paths = permalink.pathname.split('/');
- const did = await fetch(gw + 'com.atproto.identity.resolveHandle?handle=' +
- ((paths.length > 1) ? paths[2] : ''))
- .then(res => res.json()).then(async data => data.did)
- .catch(e => console.log(e));
- const uri = 'at://' + did + '/app.bsky.feed.post/' + paths.slice(-1);
- return await fetch(gw + 'app.bsky.feed.getPostThread?depth=0&uri=' + uri,
- {headers: {'Authorization':`Bearer ${jwt}`}})
- .then(res => res.json()).then(async data => {
- const appUrl = 'https://bsky.app/profile/';
- let post = {
- attachments: [],
- avatar: data.thread.post.author.avatar ?
- data.thread.post.author.avatar : '',
- // bsky.social default banner color.
- color: 'rgb(0,112,255)',
- text: data.thread.post.record.text,
- cw: '',
- date: data.thread.post.record.createdAt,
- from: {
- url: permalink,
- acct: '@' + data.thread.post.author.handle,
- display_name: data.thread.post.author.displayName ?
- data.thread.post.author.displayName
- : data.thread.post.author.handle,
- profile_url: appUrl + data.thread.post.author.handle,
- },
- };
- if (data.thread.parent) {
- post.to = {
- url: appUrl + data.thread.parent.post.author.handle + '/post/' +
- data.thread.parent.post.uri.split('/').slice(-1),
- acct: '@' + data.thread.parent.post.author.handle,
- profile_url: appUrl + data.thread.parent.post.author.handle,
- display_name: data.thread.parent.post.author.displayName ?
- data.thread.parent.post.author.displayName
- : data.thread.parent.post.author.handle,
- };
- }
- function getAttachmentsFromEmbeds(embeds) {
- let attachments = [];
- for (const eKey in embeds) {
- const embed = embeds[eKey];
- if (embed['$type'] === 'app.bsky.embed.images#view') {
- attachments = getAttachmentsFromImages(embed.images);
- } else
- if (embed['$type'] === 'app.bsky.embed.recordWithMedia#view') {
- attachments = getAttachmentsFromImages(embed.media.images);
- }
- }
- return attachments;
- }
- // app.bsky.embed.images#view
- function getAttachmentsFromImages(images) {
- let attachments = [];
- for (const iKey in images) {
- const image = images[iKey];
- const attachment = {
- tag: 'img',
- url: image.thumb,
- };
- if (image.alt) {
- attachment.alt = image.alt;
- }
- attachments.push(attachment);
- }
- return attachments;
- }
- // Embed (attachment/quote etc.)
- if (data.thread.post.embed) {
- const embed = data.thread.post.embed;
- // Attachments
- if (embed.media &&
- (embed.media['$type'] === 'app.bsky.embed.images#view')) {
- post.attachments = getAttachmentsFromImages(embed.media.images);
- }
- // Quote post
- let record = embed.record;
- if (embed['$type'] === 'app.bsky.embed.recordWithMedia#view') {
- record = embed.record.record;
- }
- if (record) {
- if (record.value) {
- post.cite = {
- attachments: [],
- avatar: record.author.avatar ? record.author.avatar : '',
- color: 'rgb(0,112,255)',
- text: record.value.text,
- cw: '',
- date: record.value.createdAt,
- from: {
- url: appUrl+record.author.handle+'/post/'+
- record.uri.split('/').slice(-1),
- acct: '@' + record.author.handle,
- display_name: record.author.displayName ?
- record.author.displayName : record.author.handle,
- profile_url: appUrl + record.author.handle,
- },
- };
- post.cite.attachments = getAttachmentsFromEmbeds(record.embeds);
- }
- }
- }
- return post;
- }).catch(e => console.log(e));
- }
- /// @sa https://pisuke-code.com/javascript-imple-htmlspecialchars/
- /// @sa http://var.blog.jp/archives/77442554.html
- function htmlspecialchars(unsafeText){
- if (typeof unsafeText !== 'string') {
- return unsafeText;
- }
- let escaped = document.createElement('div');
- escaped.textContent = unsafeText;
- console.log(escaped.innerHTML);
- return escaped.innerHTML;
- }
- };
|