Ported API client library from Mami and load colour presets through it as well.

This commit is contained in:
flash 2025-04-13 00:18:38 +00:00
parent 83dc860904
commit 6870ee3959
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
9 changed files with 256 additions and 41 deletions

View file

@ -8,6 +8,10 @@
# Sets the title of the application, might be overridden for the window title by the common settings below once they're loaded
#AMI_TITLE="Flashii Chat"
# Flashii API base URL
# Sets the base URL for Flashii API, without the version part!
#AMI_API_URL="//flashii.net/api"
# Common configuration location
# Contains further information shared by the other chat client
#FUTAMI_URL="//futami.flashii.net/common.json"

View file

@ -19,6 +19,7 @@ const exec = require('util').promisify(require('child_process').exec);
},
build: {
FUTAMI_DEBUG: isDebug,
FII_API: process.env.AMI_API_URL ?? '//flashii.net/api',
FUTAMI_URL: process.env.FUTAMI_URL ?? '//futami.flashii.net/common.json',
GIT_HASH: (await exec('git log --pretty="%H" -n1 HEAD')).stdout,
},

View file

@ -69,7 +69,7 @@ var AmiChat = function(chat, title, parent) {
var submitBox = new AmiSubmitBox(pub);
pub.submitBox = submitBox;
pub.colourPicker = new AmiColourPicker(pub);
pub.colourPicker = new AmiColourPicker(pub, chat.flashii);
if(parent !== undefined)
pub.appendTo(parent);

View file

@ -1,7 +1,7 @@
#include colour.js
#include colpick/picker.jsx
var AmiColourPicker = function(parent) {
var AmiColourPicker = function(parent, flashii) {
var container = <div id="pickerNew" class="hidden" />;
var callback = undefined;
parent.appendChild(container);
@ -17,9 +17,7 @@ var AmiColourPicker = function(parent) {
});
callback = undefined;
},
{
presets: futami.get('colours'),
},
{ flashii },
null,
function() {
callback = undefined;

View file

@ -16,7 +16,8 @@ var FwColourPicker = function(callback, options, colour, onClose) {
verifyOption('posX', 'number', -1);
verifyOption('posY', 'number', -1);
verifyOption('presets', 'object', []);
verifyOption('showPresetsTab', 'boolean', options.presets.length > 0);
verifyOption('flashii', 'object', null);
verifyOption('showPresetsTab', 'boolean', typeof options.flashii !== null || options.presets.length > 0);
verifyOption('showGridTab', 'boolean', true);
verifyOption('showSlidersTab', 'boolean', true);
verifyOption('showHexValue', 'boolean', true);
@ -141,24 +142,43 @@ var FwColourPicker = function(callback, options, colour, onClose) {
if(options.showPresetsTab)
createTab('presets', 'Presets', function() {
var presets = options.presets;
var cont = document.createElement('div');
for(var i = 0; i < presets.length; ++i)
(function(preset) {
var option = document.createElement('a');
option.href = 'javascript:void(0);';
option.className = 'fw-colour-picker-presets-option';
option.style.background = AmiColour.hex(preset.c);
option.title = preset.n;
option.onclick = function() {
setColour(preset.c);
};
onColourChange.push(function(value) {
option.classList[(value === preset.c ? 'add' : 'remove')]('fw-colour-picker-presets-option-active');
});
cont.appendChild(option);
})(presets[i]);
const appendColours = (presets, titleParam, rgbParam) => {
for(var i = 0; i < presets.length; ++i)
(function(preset) {
var option = document.createElement('a');
option.href = 'javascript:void(0);';
option.className = 'fw-colour-picker-presets-option';
option.style.background = AmiColour.hex(preset[rgbParam]);
option.title = preset[titleParam];
option.onclick = function() {
setColour(preset[rgbParam]);
};
onColourChange.push(function(value) {
option.classList[(value === preset[rgbParam] ? 'add' : 'remove')]('fw-colour-picker-presets-option-active');
});
cont.appendChild(option);
})(presets[i]);
cont.scrollTop = 0;
};
if(options.flashii !== null) {
const loading = document.createElement('div');
loading.textContent = 'Loading...';
cont.appendChild(loading);
options.flashii.v1.colours.presets({ fields: ['title', 'rgb'] })
.success(presets => {
cont.removeChild(loading);
appendColours(presets, 'title', 'rgb');
})
.fail(ex => {
loading.textContent = ex.toString();
})
.run();
} else appendColours(options.presets, 'n', 'c');
return cont;
});

View file

@ -22,18 +22,6 @@ var FutamiCommon = function(vars) {
.run();
});
},
getApiJson: function(path, noCache) {
return new Commitment((success, fail) => {
const options = { type: 'json' };
if(noCache)
options.headers = { 'Cache-Control': 'no-cache' };
$xhr.get(get('api') + path, options)
.success(({ body }) => { success(body); })
.fail(fail)
.run();
});
},
};
};

View file

@ -2,6 +2,7 @@
#include cookies.js
#include copyright.jsx
#include emotes.jsx
#include flashii.js
#include messages.jsx
#include notify.js
#include reconnect.jsx
@ -16,6 +17,9 @@
var AmiContext = function(title, auth, loading) {
var pub = {};
const flashii = new Flashii;
pub.flashii = flashii;
var settings = new AmiSettings;
pub.settings = settings;
@ -187,11 +191,12 @@ var AmiContext = function(title, auth, loading) {
return;
}
futami.getApiJson('/v1/emotes').success(emotes => {
if(Array.isArray(emotes))
emoticons.load(emotes);
chat.emoticonList.render(emoticons);
}).run();
flashii.v1.emotes({ fields: ['url', 'strings', 'min_rank'] })
.success(emotes => {
if(Array.isArray(emotes))
emoticons.load(emotes);
chat.emoticonList.render(emoticons);
}).run();
});
var reconnecter = new AmiReconnecter(chat);

200
src/ami.js/flashii.js Normal file
View file

@ -0,0 +1,200 @@
#include commitment.js
#include xhr.js
const Flashii = function(baseUrl=null) {
baseUrl ??= window.FII_API;
if(baseUrl.indexOf('//') === 0)
baseUrl = window.location.protocol + baseUrl;
const sortArray = typeof Array.prototype.toSorted === 'function'
? array => array.toSorted()
: array => {
array = array.slice();
array.sort();
return array;
};
const convertFields = fields => {
if(fields === true)
return '*';
if(Array.isArray(fields))
return sortArray(fields).join(',');
if(typeof fields === 'string')
return fields;
return null;
};
const createUrl = (path, fields=null) => {
const url = baseUrl + path;
const params = {};
if(fields)
params['fields'] = convertFields(fields);
return {
setParam(name, value) {
params[name] = value;
},
deleteParam(name) {
delete params[name];
},
toString() {
let str = url;
const queryParts = [];
for(const name in params)
if(params.hasOwnProperty(name))
queryParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(params[name]));
if(queryParts.length > 0)
str += (str.indexOf('?') < 0 ? '?' : '&') + queryParts.join('&');
return str;
},
};
};
const send = ({
method,
path,
fresh=false,
params=null,
fields=null,
body=null,
headers=null,
type='json',
}) => {
return new Commitment((success, fail) => {
const url = createUrl(path, fields);
if(params)
for(const name in params) {
if(name === 'fields')
continue;
url.setParam(name, params[name]);
}
headers ??= {};
if(fresh) headers['Cache-Control'] = 'no-cache';
const options = { type, headers };
$xhr.send(method, url, options, body)
.success(success)
.fail(fail)
.run();
});
};
const fii = {};
fii.v1 = {};
const verifyColourPresetName = name => {
if(/^([^A-Za-z0-9\-_]+)$/gu.test(name))
throw new Error('name argument is not an acceptable colour preset name.');
return name;
};
fii.v1.colours = {};
fii.v1.colours.presets = function({ fields=null, fresh=false }) {
return new Commitment((success, fail) => {
send({ method: 'GET', path: '/v1/colours/presets', fields, fresh })
.success(({ status, body }) => {
if(status === 400)
throw new Error('Fields argument contains unsupported value.');
if(status > 299)
throw new Error(`Failed to fetch colour presets with error code ${status}.`);
success(body);
})
.fail(fail)
.run();
});
};
fii.v1.colours.presets.preset = function({ name, fields=null, fresh=false }) {
return new Commitment((success, fail) => {
name = verifyColourPresetName(name);
send({ method: 'GET', path: `/v1/colours/presets/${name}`, fields, fresh })
.success(({ status, body }) => {
if(status === 400)
throw new Error('Fields argument contains unsupported value.');
if(status === 404)
throw new Error('Requested colour preset does not exist.');
if(status > 299)
throw new Error(`Failed to fetch colour preset "${name}" with error code ${status}.`);
success(body);
})
.fail(fail)
.run();
});
};
const verifyEmoticonId = id => {
if(typeof id === 'number')
id = id.toString();
if(/^([^0-9]+)$/gu.test(id))
throw new Error('id argument is not an acceptable emoticon id.');
return id;
};
fii.v1.emotes = function({ fields=null, fresh=false }) {
return new Commitment((success, fail) => {
send({ method: 'GET', path: '/v1/emotes', fields, fresh })
.success(({ status, body }) => {
if(status === 400)
throw new Error('Fields argument contains unsupported value.');
if(status > 299)
throw new Error(`Failed to fetch emoticons with error code ${status}.`);
success(body);
})
.fail(fail)
.run();
});
};
fii.v1.emotes.emote = function({ id, fields=null, fresh=false }) {
return new Commitment((success, fail) => {
id = verifyEmoticonId(id);
send({ method: 'GET', path: `/v1/emotes/${id}`, fields, fresh })
.success(({ status, body }) => {
if(status === 400)
throw new Error('Fields argument contains unsupported value.');
if(status === 404)
throw new Error('Requested emoticon does not exist.');
if(status > 299)
throw new Error(`Failed to fetch emoticon "${id}" with error code ${status}.`);
success(body);
})
.fail(fail)
.run();
});
};
fii.v1.emotes.emote.strings = function({ id, fields=null, fresh=false }) {
return new Commitment((success, fail) => {
id = verifyEmoticonId(id);
send({ method: 'GET', path: `/v1/emotes/${id}/strings`, fields, fresh })
.success(({ status, body }) => {
if(status === 400)
throw new Error('Fields argument contains unsupported value.');
if(status === 404)
throw new Error('Requested emoticon does not exist.');
if(status > 299)
throw new Error(`Failed to fetch emoticon "${id}" with error code ${status}.`);
success(body);
})
.fail(fail)
.run();
});
};
return fii;
};

View file

@ -44,8 +44,7 @@ const $xhr = (function() {
if(typeof body === 'object') {
if(body instanceof FormData) {
// content-type is implicitly set
} else if(body instanceof Blob || body instanceof ArrayBuffer
|| body instanceof DataView || body instanceof Uint8Array) {
} else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof Uint8Array) {
if(!requestHeaders.has('content-type'))
requestHeaders.set('content-type', 'application/octet-stream');
} else if(!requestHeaders.has('content-type')) {