Update MBViewer, Trasformapi

This commit is contained in:
octospacc 2024-01-24 01:04:43 +01:00
parent 0cf7946e6d
commit 3eeb0c1807
5 changed files with 216 additions and 123 deletions

View File

@ -1,5 +1,7 @@
(function(){
// NOTE: using defiant.js gives us '[undefined]' arrays instead of '[]' ones sometimes, should be fixed
const Exp = {};
let MakeTreeFromXml;
@ -8,6 +10,7 @@ const platformIsBrowser = (typeof window !== 'undefined' && typeof window.docume
if (platformIsNode && !platformIsBrowser) {
MakeTreeFromXml = (xml) => new require('jsdom').JSDOM(xml);
// TODO load all other dependencies
}
if (platformIsBrowser) {
@ -16,6 +19,10 @@ if (platformIsBrowser) {
Exp.Trasformapi = (transformerXml, initOptions={}) => {
var transformerTree = MakeTreeFromXml(transformerXml);
initOptions.sets ||= {};
for (const attr of transformerTree.querySelector(':scope > set')?.attributes) {
initOptions.sets[attr.name] = attr.value;
}
return {
TransformForInput: (entityName, upstreamName, dataIn, transformOptions) => _TransformForInput(transformerTree, initOptions, entityName, upstreamName, dataIn, transformOptions),
TransformForOutput: (entityName, upstreamName, dataIn, transformOptions) => _TransformForOutput(transformerTree, initOptions, entityName, upstreamName, dataIn, transformOptions),
@ -23,51 +30,23 @@ Exp.Trasformapi = (transformerXml, initOptions={}) => {
}
function _TransformForInput (transformerTree, initOptions, entityName, upstreamName, dataIn, transformOptions={}) {
// TODO: restructure prototype
// TODO: make the propDataType inside this function, for both main and secondary
function temp1 (upstreamName, propName, propType, propDataType, propContent, dataIn, dataOut, propNameSecondary, propTypeSecondary, propDataTypeSecondary) {
// const propDataType =
// const propDataTypeSecondary =
const dataKey = propContent.getAttribute('key');
//console.log(propName, propType, propDataType, propContent, dataIn, dataOut, propNameSecondary, propTypeSecondary, propDataTypeSecondary)
// TODO: inside here somehow happens the array error with prop > content > prop nestings, probably we need to handle secondary and primary types separately
const dataAttr = propContent.getAttribute('attr');
let dataInContent;
if (dataIn instanceof Node) {
// TODO: 'document' won't work on nodejs, must change it
//const dataNode = document.evaluate(dataKey, dataIn, ((ns) => ns), XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
//dataInContent = (dataAttr ? dataNode?.getAttribute(dataAttr) : dataNode?.textContent);
// TODO: finish this to actually handle arrays properly (even below)
const dataNodes = getElementsByXPath(dataKey, dataIn);
if (!Array.isArray(propDataTypeSecondary || propDataType) && dataNodes.length > 0) {
dataInContent = (dataAttr ? dataNodes[0].getAttribute(dataAttr) : dataNodes[0].textContent);
} else {
dataInContent = [];
for (const dataNode of dataNodes) {
// ... TODO push every item //dataInContent = (dataAttr ? dataNode?.getAttribute(dataAttr) : dataNode?.textContent);
dataInContent.push(dataAttr ? dataNodes[0].getAttribute(dataAttr) : dataNodes[0].textContent);
}
}
} else {
dataInContent = (dataKey ? _.get(dataIn, dataKey) : dataIn);
}
//const dataInContent = (dataIn instanceof Node
// ? (document.evaluate(dataKey, dataIn, (ns) => ns, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue || {}
// )['getAttribute' || 'textContent'](dataAttr)
// : (dataKey ? _.get(dataIn, dataKey) : dataIn));
// TODO: make this readable lmao
// TODO: make no type mean any/object type and remove the distinction maybe
// TODO: readd type casting
const result = (["any", "object", "string", "number", "int", "float"].includes(propTypeSecondary || propType)
? SetOrPush(dataInContent, (propDataTypeSecondary || propDataType))
// ? SetOrPush((["any", "object"].includes(propType)
// ? dataInContent
// : { int: parseInt, float: parseFloat, string: String, number: Number }[propType](dataInContent)
// ), propDataType)
: SetOrPush(MakeApiEntityObject((propTypeSecondary || propType), upstreamName, dataInContent), (propDataTypeSecondary || propDataType)));
!propNameSecondary ? (dataOut[propName] = result) : (dataOut[propName][propNameSecondary] = result);
const globalSets = { ...initOptions.sets, ...transformOptions.sets };
// due to a bug in defiant, we need to prefix something to any key starting with '@'...
// <https://stackoverflow.com/questions/68903102/renaming-object-keys-which-are-nested/68903897#68903897>
function JsonObjectKeysFix (obj) {
// TODO avoid collisions? (even if they're unlikely with what we're doing)
return (obj !== undefined && obj !== null ? Object.fromEntries(Object.entries(obj).map( ([key,value]) => {
const newKey = (key.startsWith('@') ? `_${key}` : key);
return typeof value == "object"
? [newKey, JsonObjectKeysFix(value)]
: [newKey, value]
})) : obj);
}
function MakeApiEntityObject (entityName, upstreamName, dataIn) {
if (!dataIn) {
// nothing to do
return;
};
let dataOut = {};
const entitySchema = transformerTree.querySelector(`:scope > entity[name="${entityName}"]`);
for (const propSchema of entitySchema.querySelectorAll(':scope > prop')) {
@ -79,41 +58,74 @@ function _TransformForInput (transformerTree, initOptions, entityName, upstreamN
// property is not implemented for the current upstream, skip it
continue;
}
const propContentChildren = propContent.querySelectorAll(`:scope > prop`); // TODO
const propContentChildren = propContent.querySelectorAll(`:scope > prop`);
if (propContentChildren.length === 0) {
//const dataKey = propContent.getAttribute('key');
//const dataAttr = propContent.getAttribute('attr');
//const dataInContent = (dataIn instanceof Node
// // TODO: 'document' won't work on nodejs, must change it
// ? (document.evaluate(dataKey, dataIn, (ns) => ns, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue || {}
// )[dataAttr || 'textContent']
// : (dataKey ? _.get(dataIn, dataKey) : dataIn));
//dataOut[propName] = (["string", "number", "int", "float"].includes(propType)
// ? SetOrPush(dataInContent, propDataType)
// : SetOrPush(MakeApiEntityObject(propType, upstreamName, dataInContent), propDataType));
temp1(upstreamName, propName, propType, propDataType, propContent, dataIn, dataOut);
const dataKey = SubstituteStringSets(propContent.getAttribute('query'), globalSets);
const dataInContent = (dataIn instanceof Node
? GetElementsByXPath(dataKey, dataIn)[0]?.textContent
: (dataKey ? /*_.get*/defiant.search(dataIn, dataKey)[0] : dataIn)
);
// I don't know if this is fully correct
if (Array.isArray(propDataType) && Array.isArray(dataInContent)) {
for (const itemContent of dataInContent) {
dataOut[propName] = (["string", "number", "int", "float"].includes(propType)
? SetOrPush(itemContent, propDataType)
: SetOrPush(MakeApiEntityObject(propType, upstreamName, itemContent), propDataType)
);
}
} else {
dataOut[propName] = (["string", "number", "int", "float"].includes(propType)
? SetOrPush(dataInContent, propDataType)
: SetOrPush(MakeApiEntityObject(propType, upstreamName, dataInContent), propDataType)
);
}
} else {
dataOut[propName] = {}; // should this be an array in some cases or not?
// TODO: wrap this and the above in a function, to allow for code reuse, right now the else condition does less things than what it should because of the duplication
dataOut[propName] = {}; // NOTE: in some cases, this should be an array, I guess, or maybe not?
for (const propChildSchema of propContentChildren) {
const entityChildSchema = transformerTree.querySelector(`:scope > entity[name="${propType}"]`);
const propChildName = propChildSchema.getAttribute('name');
const propChildProp = entityChildSchema.querySelector(`:scope > prop[name="${propChildName}"]`);
const propChildType = propChildProp.getAttribute('type').split('[]')[0];
const propChildDataType = (propChildProp.getAttribute('type').endsWith('[]') ? [] : {});
//const childDataKey = propChildSchema.getAttribute('key');
//const childDataInContent = childDataKey ? _.get(dataIn, childDataKey) : dataIn;
//dataOut[propName][propChildName] = (["string", "number", "int", "float"].includes(propChildType)
// ? SetOrPush(childDataInContent, propDataType)
// : null); // TODO other recursions? //SetOrPush(MakeApiEntityObject(propType, upstreamName, childDataInContent), childDataInContent));
temp1(upstreamName, propName, propType, propDataType, propChildSchema, dataIn, dataOut, propChildName, propChildType, propChildDataType);
const childDataKey = SubstituteStringSets(propChildSchema.getAttribute('query'), globalSets);
let childDataInContent = [];
if (dataIn instanceof Node) {
const nodes = GetElementsByXPath(childDataKey, dataIn);
if (nodes.length === 1) {
childDataInContent = nodes[0]?.textContent
} else {
for (const node of nodes) {
childDataInContent.push(node?.textContent);
}
}
} else {
childDataInContent = (childDataKey ? /*_.get*/defiant.search(dataIn, childDataKey)[0] : dataIn);
}
const childResult = (["string", "number", "int", "float"].includes(propChildType)
? childDataInContent
: MakeApiEntityObject(propChildType, upstreamName, childDataInContent)
);
if (Array.isArray(propDataType)) {
if (!Array.isArray(dataOut[propName])) {
dataOut[propName] = [];
}
const childItems = SureArray(childResult);
for (const childItemIndex in childItems) {
const childItem = childItems[childItemIndex];
if (!dataOut[propName][childItemIndex]) {
dataOut[propName][childItemIndex] = {};
}
dataOut[propName][childItemIndex][propChildName] = childItem;
}
} else {
dataOut[propName][propChildName] = childResult;
}
}
}
}
//console.log(dataOut);
return dataOut;
}
return MakeApiEntityObject (entityName, upstreamName, dataIn);
return MakeApiEntityObject (entityName, upstreamName, (dataIn instanceof Node ? dataIn : JsonObjectKeysFix(dataIn)));
}
function _TransformForOutput (transformerTree, initOptions, entityName, upstreamName, dataIn, transformOptions={}) {
@ -122,7 +134,7 @@ function _TransformForOutput (transformerTree, initOptions, entityName, upstream
// <https://stackoverflow.com/questions/36303869/how-to-use-document-evaluate-and-xpath-to-get-a-list-of-elements/42600459#42600459>
// TODO: 'document' won't work on nodejs, must change it
function getElementsByXPath (xpath, parent) {
function GetElementsByXPath (xpath, parent) {
let results = [];
let query = document.evaluate(xpath, parent || document, ((ns) => ns), XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i=0, length=query.snapshotLength; i<length; ++i) {
@ -131,7 +143,16 @@ function getElementsByXPath (xpath, parent) {
return results;
}
const SetOrPush = (item, dest) => Array.isArray(dest) ? [...dest, item] : item;
const SetOrPush = (item, dest) => (Array.isArray(dest) ? [...dest, item] : item);
const SureArray = (item) => (Array.isArray(item) ? item : [item]);
const SubstituteStringSets = (string, sets) => {
for (const set in sets) {
string = string?.replaceAll(`{${set}}`, sets[set]);
}
return string;
}
if (platformIsNode) module.exports = Exp;
if (platformIsBrowser) window.Trasformapi = Exp.Trasformapi;

6
public/Assets/Lib/defiant.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -205,6 +205,7 @@
<script src="js/widget-frame.js"></script>
<script src="js/telegram-web.js"></script>
<script src="../Assets/Lib/lodash.custom.min.js"></script>
<script src="../Assets/Lib/defiant.min.js"></script>
<script src="../Assets/Lib/Trasformapi.js"></script>
<script src="./js/TrasformapiSchema.js.xml"></script>
<script src="./js/MBViewer.js"></script>

View File

@ -77,12 +77,15 @@ function MakeSiteRestUrl (path='') {
return `https://${proxies[~~(Math.random() * proxies.length)]}/?${siteUrl}/${MbState.platform === 'wordpress.org' ? `wp-json/${path}` : ''}`;
} else if (MbState.platform === 'wordpress.com') {
return `https://public-api.wordpress.com/rest/v1.1/sites/${GetDomainFromUrl(siteUrl)}/${path}`;
} else if (MbState.platform === 'mastodon') {
return `${MbState.siteUrl.split('/').slice(0, 3).join('/')}/api/${path || 'v2/instance'}`;
}
}
}
function MakeApiEndpoint (type, options={}) {
const translations = {
"mastodon": {},
"wordpress.org": {
count: "per_page",
orderBy: "orderby",
@ -100,6 +103,13 @@ function MakeApiEndpoint (type, options={}) {
}
query = `${options.id || ''}?${query.slice(1)}`;
switch (MbState.platform) {
case 'mastodon':
switch (type) {
case 'acct' : query = `v1/accounts/lookup?acct=${options.username}`; break;
case 'default':
case 'posts': query = `v1/accounts/${MbState.userId}/statuses?exclude_replies=true`; break;
}
break;
case 'wordpress.org': query = `wp/v2/${type}/${query}`; break;
case 'wordpress.com': query = `${type}/${query}`; break;
}
@ -201,7 +211,18 @@ async function MbViewerInit () {
MbState.siteData = (["atom", "rss"].includes(MbState.platform)
? new DOMParser().parseFromString(await siteRequest.text(), 'text/xml')
: await siteRequest.json());
if (MbState.platform === 'mastodon') {
MbState.siteData = MbApiTransformer('profile', MbState.platform, MbState.siteData);
let username = MbState.siteUrl;
if (username.endsWith('/')) username = username.slice(0, -1);
username = username.split('/').slice(-1)[0];
if (username.startsWith('@')) username = username.slice(1);
const userRequest = await fetch(MakeSiteRestUrl(MakeApiEndpoint('acct', { username })));
const userData = await userRequest.json();
MbState.authors[MbState.userId = userData.id] = MbApiTransformer('profile', MbState.platform, userData);
}
} catch(err) {
console.log(err);
setTimeout(MbViewerInit, 1000);
return;
}
@ -216,6 +237,7 @@ async function MbViewerInit () {
TWeb.loadMore($('.js-messages_more_wrap > a[data-after]'), MbState.startingPost);
$('section.tgme_channel_history.js-message_history').prepend(MakeMoreWrapperHtml('before'));
} catch(err) {
console.log(err);
setTimeout(MbViewerInit, 1000);
return;
}
@ -228,16 +250,17 @@ async function MbViewerInit () {
$('.tgme_channel_info_header_username').html(`<a href="${siteLink}">${GetDomainFromUrl(siteLink).toLowerCase()}</a>`);
$('a[name="goBack"]')[0].hidden = false;
}
MbState.siteData.iconUrl = (MbState.siteData.site_icon_url || MbState.siteData.icon?.img || MbState.siteData.icon?.ico);
if (["atom", "rss"].includes(MbState.platform)) {
$('section.tgme_channel_history.js-message_history').html(MakeMoreWrapperHtml());
TWeb.loadMore($('.js-messages_more_wrap > a'), MbState.siteData);
MbState.siteData = MbApiTransformer('profile', MbState.platform, MbState.siteData.querySelector(':scope > channel'));
}
MbState.siteData.iconUrl = (MbState.siteData.icon?.url || MbState.siteData.site_icon_url || MbState.siteData.icon?.img || MbState.siteData.icon?.ico);
MbState.siteData.acroName ||= (!MbState.siteData.iconUrl ? MbState.siteData.name && MakeAcroName(MbState.siteData.name) : '');
MbState.siteData.bgColor = ~~(Math.random() * 7);
if (MbState.siteData.iconUrl && !["http", "https"].includes(MbState.siteData.iconUrl.split('://')[0])) {
MbState.siteData.iconUrl = `${MbState.siteUrl}${MbState.siteData.iconUrl}`;
}
if (["atom", "rss"].includes(MbState.platform)) {
$('section.tgme_channel_history.js-message_history').html(MakeMoreWrapperHtml());
TWeb.loadMore($('.js-messages_more_wrap > a'), MbState.siteData);
}
if (!MbState.siteUrl) {
$('a[name="goBack"]')[0].hidden = true;
$('section.tgme_channel_history.js-message_history').html(MakeMoreWrapperHtml());
@ -281,6 +304,12 @@ async function MbViewerInit () {
<br/> * Initial support for handling data via Trasformapi lib
<br/> * Initial, experimental support for RSS feeds specifically, via Transformapi (very broken)
</p>`, time: '2024-01-23T01:00' }, { content: `<p>
New changes:
<br/> * Updated Trasformapi.js with misc fixes, query constants, and streamlined/powerful data querying
(XPath support for both XML sources, and JSON sources via defiant.js)
<br/> * Only slightly better RSS support
<br/> * Initial, experimental support for Mastodon profiles (broken)
</p>`, time: '2024-01-24T01:00' }, { content: `<p>
Copyright notice: MBViewer uses code borrowed from <a href="https://t.me">t.me</a>,
specially modified to handle customized data visualizations in an MB-style.
<br/>
@ -288,6 +317,7 @@ async function MbViewerInit () {
all rights upon the original materials (which are: everything not strictly related to the "MBViewer" mod) belong to the original owners.
</p>` }]);
}
document.title = `${MbState.siteData.name} — 👁️‍🗨️️ MBViewer`;
$('.tgme_page_photo_image').attr('data-content', MbState.siteData.acroName);
$('.tgme_header_title, .tgme_channel_info_header_title').html(MbState.siteData.name);
$('.tgme_channel_info_description').html(MbState.siteData.description);
@ -324,7 +354,10 @@ function MakeMoreWrapperHtml (wrapType) {
async function MakeMbHtml (postData, makeMoreWrap) {
postData = (typeof(postData) === 'string' ? JSON.parse(postData) : postData);
if (["atom", "rss"].includes(MbState.platform)) {
postData = Array.from(postData.querySelectorAll(':scope > channel > item')).reverse();
postData = Array.from(postData.querySelectorAll(':scope > channel > item'));
}
if (["atom", "rss", "mastodon"].includes(MbState.platform)) {
postData.reverse();
}
let html = '';
const siteLink = (MbState.siteData.url || MbState.siteData.URL || MbState.siteLink);
@ -343,13 +376,15 @@ async function MakeMbHtml (postData, makeMoreWrap) {
const authorLink = (authorData?.link || (siteLink && `${siteLink}/author/${authorData?.name}`));
const authorHref = (authorLink ? `href="${authorLink}"` : '');
const iconUrl = (Object.values(authorData?.avatar_urls || {}).slice(-1)[0] || authorData?.icon?.url || MbState.siteData.iconUrl);
//let attachmentsHtml = '';
// TODO change this after fixing Trasformapi
//for (const attachment of postData.attachments?.url) {
//for (const attachment of postData.attachments) {
// TODO more media types
// attachmentsHtml += `<img src="${attachment.url}"/>`;
//}
let attachmentsHtml = '';
for (const attachment of (postData.attachments || postData.quoting?.attachments || [])) {
if (attachment) {
const mediaKind = attachment.type?.split('/')[0];
const elemTag = (mediaKind === 'image' ? 'img' : mediaKind);
const elemClosing = (mediaKind === 'image' ? '/>' : `></${elemTag}>`);
attachmentsHtml += `<${elemTag} controls="true" src="${attachment.url}" alt="${attachment.description?.replaceAll('&', '&amp;')?.replaceAll('"', '&quot;') || ''}"/>`;
}
}
html += `
<div class="tgme_widget_message_wrap js-widget_message_wrap date_visible">
<div class="tgme_widget_message text_not_supported_wrap js-widget_message" data-post="${postData.id || postData.ID}">
@ -383,8 +418,9 @@ async function MakeMbHtml (postData, makeMoreWrap) {
</div>
<div class="tgme_widget_message_text js-message_text before_footer" dir="auto">
<div class="MbPost">
<!--${/*attachmentsHtml*/JSON.stringify(postData.attachments)}-->
${ReformatPostHtml(postData.content?.rendered || postData.content)}
${attachmentsHtml}
${ReformatPostHtml(postData.content)}
${postData.quoting ? `[♻️ Reblog]: ${ReformatPostHtml(postData.quoting.content)}` : ''}
</div>
</div>
<div class="tgme_widget_message_footer compact js-message_footer">

View File

@ -1,5 +1,9 @@
const MbViewerTrasformapiSchema = `<schema>
<set
rss-media-query="*[name()='media:content' or name()='enclosure']"
/>
<!-- WIP, find out how to structure this -->
<endpoint name="messages">
<method name="GET" args="" returns="message[]"/>
@ -8,49 +12,55 @@ const MbViewerTrasformapiSchema = `<schema>
<entity name="message">
<prop name="id" type="int">
<content upstream="rss" key="./guid"/>
<content upstream="wordpress.com" key="ID"/>
<content upstream="wordpress.org" key="id"/>
<content upstream="mastodon" key="id"/>
<content upstream="rss" query="./guid"/>
<content upstream="wordpress.com" query="//ID"/>
<content upstream="wordpress.org" query="//id"/>
<content upstream="mastodon" query="//id"/>
</prop>
<prop name="url" type="string">
<content upstream="rss" key="./link"/>
<content upstream="wordpress.com" key="URL"/>
<content upstream="wordpress.org" key="link"/>
<content upstream="mastodon" key="url"/>
<content upstream="rss" query="./link"/>
<content upstream="wordpress.com" query="//URL"/>
<content upstream="wordpress.org" query="//link"/>
<content upstream="mastodon" query="//url"/>
</prop>
<prop name="title" type="string">
<content upstream="rss" key="title"/>
<content upstream="wordpress.com" key="title"/>
<content upstream="wordpress.org" key="title.rendered"/>
<content upstream="rss" query="title"/>
<content upstream="wordpress.com" query="//title"/>
<content upstream="wordpress.org" query="//title/rendered"/>
</prop>
<prop name="content" type="string">
<!-- TODO optional multiple 'key' attrs -->
<!--<content upstream="rss" key="content:encoded"/>-->
<content upstream="rss" key="./description"/>
<content upstream="wordpress.com" key="content"/>
<content upstream="wordpress.org" key="content.rendered"/>
<content upstream="mastodon" key="url"/>
<!-- TODO optional multiple 'query' attrs -->
<!--<content upstream="rss" query="content:encoded"/>-->
<content upstream="rss" query="./description"/>
<content upstream="wordpress.com" query="//content"/>
<content upstream="wordpress.org" query="//content/rendered"/>
<content upstream="mastodon" query="//content"/>
</prop>
<!-- TODO: fix this, it's broken with somehow we ending up with an object with urls array, not an attachments array -->
<prop name="attachments" type="file[]">
<!--<content upstream="rss" key="./media:content"/>-->
<content upstream="rss">
<prop name="url" key="*[name()='media:content']" attr="url"/>
<prop name="url" query="{rss-media-query}/@url"/>
<prop name="type" query="{rss-media-query}/@type"/>
<prop name="description" query="{rss-media-query}/*[name()='media:description']"/>
</content>
<!--<content upstream="mastodon" key="media_attachments"/>-->
<!--
<content upstream="mastodon">
<prop name="url" query="media_attachments.url"/>
<prop name="type" query="media_attachments.type"/>
</content>
-->
<content upstream="mastodon" query="//media_attachments"/>
</prop>
<prop name="author" type="profile">
<content upstream="rss"/>
<content upstream="wordpress.com" key="author"/>
<content upstream="wordpress.com" query="//author"/>
<content upstream="wordpress.org"/>
<content upstream="mastodon" key="account"/>
<content upstream="mastodon" query="//account"/>
</prop>
<prop name="time" type="string">
<content upstream="rss" key="pubDate"/>
<content upstream="wordpress.com" key="date"/>
<content upstream="wordpress.org" key="date"/>
<content upstream="mastodon" key="created_at"/>
<content upstream="rss" query="pubDate"/>
<content upstream="wordpress.com" query="//date"/>
<content upstream="wordpress.org" query="//date"/>
<content upstream="mastodon" query="//created_at"/>
</prop>
<prop name="revisions" type="revision[]">
<content upstream="wordpress.com"/>
@ -58,44 +68,63 @@ const MbViewerTrasformapiSchema = `<schema>
<content upstream="mastodon"/>
</prop>
<prop name="quoting" type="message">
<content upstream="mastodon" key="reblog"/>
<content upstream="mastodon" query="//reblog"/>
</prop>
<!--<prop name="replying" type="message">
<content upstream="mastodon" key=""/>
<content upstream="mastodon" query=""/>
</prop>-->
</entity>
<entity name="revision">
<prop name="time" type="string">
<content upstream="wordpress.com" key="modified"/>
<content upstream="wordpress.org" key="modified"/>
<content upstream="mastodon" key="edited_at"/>
<content upstream="wordpress.com" query="//modified"/>
<content upstream="wordpress.org" query="//modified"/>
<content upstream="mastodon" query="//edited_at"/>
</prop>
</entity>
<!-- TODO (for wordpress) how to handle both authors and sites as a profile type? maybe add a 'variant' attr for 'content' tags? -->
<entity name="profile">
<prop name="id" type="int">
<content upstream="wordpress.com" key="ID"/>
<content upstream="wordpress.org" key="author"/>
<prop name="id" type="int"> <!-- TODO fix type -->
<content upstream="rss" query="link"/>
<content upstream="wordpress.com" query="//ID"/>
<content upstream="wordpress.org" query="//author"/>
</prop>
<prop name="url" type="string">
<content upstream="wordpress.com" key="profile_URL"/>
<content upstream="rss" query="link"/>
<content upstream="wordpress.com" query="//profile_URL"/>
</prop>
<prop name="name" type="string">
<!--<content upstream="rss" key="dc:creator"/>-->
<content upstream="rss" key="dc:creator"/>
<content upstream="wordpress.com" key="name"/>
<content upstream="rss" query="*[name()='title' or name()='dc:creator']"/>
<content upstream="wordpress.com" query="//name"/>
<content upstream="mastodon" query="//title"/>
</prop>
<prop name="description" type="string">
<content upstream="rss" query="description"/>
<content upstream="mastodon" query="//description"/>
</prop>
<prop name="icon" type="file">
<content upstream="rss">
<prop name="url" query="image/url"/>
</content>
<content upstream="wordpress.com">
<prop name="url" key="avatar_URL"/>
<prop name="url" query="//avatar_URL"/>
</content>
<content upstream="mastodon"> <!-- TODO read user avatars -->
<!--<prop name="url" query="//thumbnail/url"/>-->
<prop name="url" query="//contact/account/avatar"/>
</content>
</prop>
</entity>
<entity name="file">
<prop name="url" type="string"/>
<prop name="url" type="string">
<content upstream="mastodon" query="//url"/>
</prop>
<prop name="type" type="string">
<content upstream="mastodon" query="//type"/>
</prop>
<prop name="description" type="string"/>
</entity>
</schema>`;