inital (feature: post likes)

This commit is contained in:
YK 2024-08-31 06:18:16 +03:00
commit 3bdb2ebef5
24 changed files with 3004 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
rs/target
/unpacked_*
*.rq

30
build.sh Executable file
View File

@ -0,0 +1,30 @@
## --release or --dev - exclude/include debug info
## --no-typescript - disable .d.ts files output
## --out-dir - where to write the compiled files
## --out-name - force output file names
## --target - always use "web"!
## See https://rustwasm.github.io/wasm-pack/book/commands/build.html
echo Building wasm module...
wasm-pack build rs --dev --no-typescript --out-dir "../ext/js/wasm" --out-name "wasm_mod" --target web
## wasm-pack creates bunch of useless files:
echo Removing trash files...
rm -f ext/js/wasm/.gitignore
rm -f ext/js/wasm/package.json
## create chrome package and exclude manifest for firefox
## see ReadMe for more info on manifest config
## subshell call with cd is required to avoid placing /extension/ folder as the root
rm -f chrome.zip && \
(cd ext && zip -rq ../chrome.zip . -x manifest_ff.json -x manifest.json) && \
printf "@ manifest_cr.json\n@=manifest.json\n" | zipnote -w chrome.zip && \
echo Chrome package: chrome.zip
## create firefox package, exclude chrome manifest and rename FF manifest to its default file name
rm -f firefox.zip && \
(cd ext && zip -rq ../firefox.zip . -x manifest_cr.json -x manifest.json) && \
printf "@ manifest_ff.json\n@=manifest.json\n" | zipnote -w firefox.zip && \
echo Firefox package: firefox.zip
rm -rf unpacked_ch && unzip chrome.zip -d unpacked_ch

BIN
chrome.zip Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ext/assets/Inter_Tight.zip Normal file

Binary file not shown.

BIN
ext/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
ext/assets/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
ext/assets/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

BIN
ext/assets/icon32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
ext/assets/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

57
ext/js/background.js Normal file
View File

@ -0,0 +1,57 @@
import initWasmModule, { post, like } from './wasm/wasm_mod.js';
(async () => {
await initWasmModule();
})();
chrome.runtime.onMessage.addListener(async (request, sender, _) => {
if (request?.action) {
const storage = await chrome.storage.local.get();
if (request.action == "like") {
const { rect, ratio, by, me, id, is_private } = request;
if (sender.tab) {
// make a screenshot
if (storage.screenshot) {
setTimeout(async function () {
const screenshot = await chrome.tabs.captureVisibleTab(null, { format: "png" });
const canvas = new OffscreenCanvas(rect.width, rect.height);
const bytes = atob(screenshot.replace("data:image/png;base64,", ""));
const array_buf = new ArrayBuffer(bytes.length);
const uint_arr = new Uint8Array(array_buf);
for (let i = 0; i < bytes.length; i++) {
uint_arr[i] = bytes.charCodeAt(i);
}
const blob = new Blob([array_buf], { type: "image/png" });
const top = Math.max(rect.top, 0);
const bitmap = await createImageBitmap(
blob,
Math.ceil(rect.left * ratio),
Math.ceil(top * ratio),
rect.width * ratio,
(rect.bottom - top) * ratio // if top of the image is outside of the viewport, subtract offset from height
);
canvas.getContext('bitmaprenderer').transferFromImageBitmap(bitmap);
const cropped_blob = await canvas.convertToBlob(); // slllloooow. kinda. 300ms.
const cropped_bytes = await cropped_blob.arrayBuffer();
const cropped_bytes_uint8 = new Uint8Array(cropped_bytes);
await like({ id, by, me, is_private }, cropped_bytes_uint8);
}, storage.delay || 1)
} else {
await like({ id, by, me, is_private });
}
}
}
}
})

58
ext/js/content-script.js Normal file
View File

@ -0,0 +1,58 @@
const obs_cb = (ml, _) => {
ml.forEach((mutation) => {
if (mutation.type === "attributes" && mutation.attributeName === "data-testid") {
const old = mutation.oldValue.toLocaleLowerCase();
const _new = mutation.target.dataset.testid.toLocaleLowerCase();
if (old == "like" && _new == "unlike") {
const tweet = mutation.target.closest('article[data-testid="tweet"]');
if (tweet) {
const rect = tweet.getBoundingClientRect();
const ratio = window.devicePixelRatio;
const parts = tweet.querySelector("time")?.closest("a")?.href?.split("/");
if (parts && parts.length > 5) {
const id = parts[5];
const by = {
name: tweet.querySelector("div[data-testid='User-Name'] a")?.innerText,
username: parts[3]
};
const me_data = document.querySelector("button[aria-label='Account menu']")?.innerText.split("\n");
const me = me_data && me_data.length > 1 ? {
name: me_data[0],
username: me_data[1]
}: undefined;
const is_private = tweet.querySelector("div[data-testid='User-Name'] svg[data-testid='icon-lock']") !== null;
chrome.runtime.sendMessage({ action: "like", rect, ratio, id, by, me, is_private });
}
}
}
// @TODO: unlike feature!
}
});
};
const obs = new MutationObserver(obs_cb);
const config = {
subtree: true,
attributeFilter: ["data-testid"],
attributeOldValue: true,
};
obs.observe(document, config);

27
ext/js/popup.js Normal file
View File

@ -0,0 +1,27 @@
function resolve_field (type) {
return type === "checkbox" ? "checked" : "value";
}
document.addEventListener('DOMContentLoaded', async function () {
const names = ["token", "group", "header", "domain", "blacklist", /*"delay",*/ "posts", "likes", "private", "screenshot"];
const storage = await chrome.storage.local.get();
for (let i = 0; i < names.length; i++) {
const name = names[i];
const element = document.getElementById(name);
if (element) {
const field = resolve_field(element.type);
let saved = storage[name];
if (saved) {
element[field] = saved;
}
element.addEventListener("change", async (evt) => {
await chrome.storage.local.set({ [name]: evt.target[field] });
});
}
}
});

751
ext/js/wasm/wasm_mod.js Normal file
View File

@ -0,0 +1,751 @@
let wasm;
const heap = new Array(128).fill(undefined);
heap.push(undefined, null, true, false);
function getObject(idx) { return heap[idx]; }
function _assertBoolean(n) {
if (typeof(n) !== 'boolean') {
throw new Error(`expected a boolean argument, found ${typeof(n)}`);
}
}
function _assertNum(n) {
if (typeof(n) !== 'number') throw new Error(`expected a number argument, found ${typeof(n)}`);
}
let WASM_VECTOR_LEN = 0;
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (typeof(arg) !== 'string') throw new Error(`expected a string argument, found ${typeof(arg)}`);
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
if (ret.read !== arg.length) throw new Error('failed to pass whole string');
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
let heap_next = heap.length;
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
if (typeof(heap_next) !== 'number') throw new Error('corrupt heap');
heap[idx] = obj;
return idx;
}
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
function debugString(val) {
// primitive types
const type = typeof val;
if (type == 'number' || type == 'boolean' || val == null) {
return `${val}`;
}
if (type == 'string') {
return `"${val}"`;
}
if (type == 'symbol') {
const description = val.description;
if (description == null) {
return 'Symbol';
} else {
return `Symbol(${description})`;
}
}
if (type == 'function') {
const name = val.name;
if (typeof name == 'string' && name.length > 0) {
return `Function(${name})`;
} else {
return 'Function';
}
}
// objects
if (Array.isArray(val)) {
const length = val.length;
let debug = '[';
if (length > 0) {
debug += debugString(val[0]);
}
for(let i = 1; i < length; i++) {
debug += ', ' + debugString(val[i]);
}
debug += ']';
return debug;
}
// Test for built-in
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
let className;
if (builtInMatches.length > 1) {
className = builtInMatches[1];
} else {
// Failed to match the standard '[object ClassName]'
return toString.call(val);
}
if (className == 'Object') {
// we're a user defined class or Object
// JSON.stringify avoids problems with cycles, and is generally much
// easier than looping through ownProperties of `val`.
try {
return 'Object(' + JSON.stringify(val) + ')';
} catch (_) {
return 'Object';
}
}
// errors
if (val instanceof Error) {
return `${val.name}: ${val.message}\n${val.stack}`;
}
// TODO we could test for more things here, like `Set`s and `Map`s.
return className;
}
const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(state => {
wasm.__wbindgen_export_2.get(state.dtor)(state.a, state.b)
});
function makeMutClosure(arg0, arg1, dtor, f) {
const state = { a: arg0, b: arg1, cnt: 1, dtor };
const real = (...args) => {
// First up with a closure we increment the internal reference
// count. This ensures that the Rust closure environment won't
// be deallocated while we're invoking it.
state.cnt++;
const a = state.a;
state.a = 0;
try {
return f(a, state.b, ...args);
} finally {
if (--state.cnt === 0) {
wasm.__wbindgen_export_2.get(state.dtor)(a, state.b);
CLOSURE_DTORS.unregister(state);
} else {
state.a = a;
}
}
};
real.original = state;
CLOSURE_DTORS.register(real, state, state);
return real;
}
function logError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
let error = (function () {
try {
return e instanceof Error ? `${e.message}\n\nStack:\n${e.stack}` : e.toString();
} catch(_) {
return "<failed to stringify thrown value>";
}
}());
console.error("wasm-bindgen: imported JS function that was not marked as `catch` threw an error:", error);
throw e;
}
}
function __wbg_adapter_36(arg0, arg1, arg2) {
_assertNum(arg0);
_assertNum(arg1);
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6ca29b9613afd685(arg0, arg1, addHeapObject(arg2));
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
wasm.__wbindgen_exn_store(addHeapObject(e));
}
}
/**
* @param {string} link
*/
export function post(link) {
const ptr0 = passStringToWasm0(link, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
wasm.post(ptr0, len0);
}
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1, 1) >>> 0;
getUint8ArrayMemory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
/**
* @param {any} tweet
* @param {Uint8Array | undefined} [image]
* @returns {Promise<number | undefined>}
*/
export function like(tweet, image) {
var ptr0 = isLikeNone(image) ? 0 : passArray8ToWasm0(image, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
const ret = wasm.like(addHeapObject(tweet), ptr0, len0);
return takeObject(ret);
}
function __wbg_adapter_123(arg0, arg1, arg2, arg3) {
_assertNum(arg0);
_assertNum(arg1);
wasm.wasm_bindgen__convert__closures__invoke2_mut__h8b3bb179cc2990bd(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_get_36a92e2d56d3a556 = function() { return handleError(function (arg0) {
const ret = chrome.storage.local.get(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_is_undefined = function(arg0) {
const ret = getObject(arg0) === undefined;
_assertBoolean(ret);
return ret;
};
imports.wbg.__wbindgen_in = function(arg0, arg1) {
const ret = getObject(arg0) in getObject(arg1);
_assertBoolean(ret);
return ret;
};
imports.wbg.__wbindgen_boolean_get = function(arg0) {
const v = getObject(arg0);
const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2;
_assertNum(ret);
return ret;
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof(obj) === 'string' ? obj : undefined;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_is_object = function(arg0) {
const val = getObject(arg0);
const ret = typeof(val) === 'object' && val !== null;
_assertBoolean(ret);
return ret;
};
imports.wbg.__wbg_log_7e890cdad20ab0b3 = function() { return logError(function (arg0, arg1) {
console.log(getStringFromWasm0(arg0, arg1));
}, arguments) };
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_number_get = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof(obj) === 'number' ? obj : undefined;
if (!isLikeNone(ret)) {
_assertNum(ret);
}
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
};
imports.wbg.__wbindgen_number_new = function(arg0) {
const ret = arg0;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {
const ret = getObject(arg0) == getObject(arg1);
_assertBoolean(ret);
return ret;
};
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
const ret = getObject(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbg_getwithrefkey_edc2c8960f0f1191 = function() { return logError(function (arg0, arg1) {
const ret = getObject(arg0)[getObject(arg1)];
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_fetch_25e3a297f7b04639 = function() { return logError(function (arg0) {
const ret = fetch(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_is_function = function(arg0) {
const ret = typeof(getObject(arg0)) === 'function';
_assertBoolean(ret);
return ret;
};
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = takeObject(arg0).original;
if (obj.cnt-- == 1) {
obj.a = 0;
return true;
}
const ret = false;
_assertBoolean(ret);
return ret;
};
imports.wbg.__wbg_queueMicrotask_12a30234db4045d3 = function() { return logError(function (arg0) {
queueMicrotask(getObject(arg0));
}, arguments) };
imports.wbg.__wbg_queueMicrotask_48421b3cc9052b68 = function() { return logError(function (arg0) {
const ret = getObject(arg0).queueMicrotask;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_signal_41e46ccad44bb5e2 = function() { return logError(function (arg0) {
const ret = getObject(arg0).signal;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_new_ebf2727385ee825c = function() { return handleError(function () {
const ret = new AbortController();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_abort_8659d889a7877ae3 = function() { return logError(function (arg0) {
getObject(arg0).abort();
}, arguments) };
imports.wbg.__wbg_settype_b6ab7b74bd1908a1 = function() { return logError(function (arg0, arg1, arg2) {
getObject(arg0).type = getStringFromWasm0(arg1, arg2);
}, arguments) };
imports.wbg.__wbg_newwithstrandinit_a31c69e4cc337183 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_setbody_734cb3d7ee8e6e96 = function() { return logError(function (arg0, arg1) {
getObject(arg0).body = getObject(arg1);
}, arguments) };
imports.wbg.__wbg_setcredentials_2b67800db3f7b621 = function() { return logError(function (arg0, arg1) {
getObject(arg0).credentials = ["omit","same-origin","include",][arg1];
}, arguments) };
imports.wbg.__wbg_setheaders_be10a5ab566fd06f = function() { return logError(function (arg0, arg1) {
getObject(arg0).headers = getObject(arg1);
}, arguments) };
imports.wbg.__wbg_setmethod_dc68a742c2db5c6a = function() { return logError(function (arg0, arg1, arg2) {
getObject(arg0).method = getStringFromWasm0(arg1, arg2);
}, arguments) };
imports.wbg.__wbg_setmode_a781aae2bd3df202 = function() { return logError(function (arg0, arg1) {
getObject(arg0).mode = ["same-origin","no-cors","cors","navigate",][arg1];
}, arguments) };
imports.wbg.__wbg_setsignal_91c4e8ebd04eb935 = function() { return logError(function (arg0, arg1) {
getObject(arg0).signal = getObject(arg1);
}, arguments) };
imports.wbg.__wbg_fetch_ba7fe179e527d942 = function() { return logError(function (arg0, arg1) {
const ret = getObject(arg0).fetch(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_newwithu8arraysequenceandoptions_c8bc456a23f02fca = function() { return handleError(function (arg0, arg1) {
const ret = new Blob(getObject(arg0), getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_new_f9f1d655d855a601 = function() { return handleError(function () {
const ret = new FormData();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_append_876bddfd2c8b42fb = function() { return handleError(function (arg0, arg1, arg2, arg3) {
getObject(arg0).append(getStringFromWasm0(arg1, arg2), getObject(arg3));
}, arguments) };
imports.wbg.__wbg_append_fc486ec9757bf1c1 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) {
getObject(arg0).append(getStringFromWasm0(arg1, arg2), getObject(arg3), getStringFromWasm0(arg4, arg5));
}, arguments) };
imports.wbg.__wbg_append_b10805b72af15312 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_new_e27c93803e1acc42 = function() { return handleError(function () {
const ret = new Headers();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_append_f3a4426bb50622c5 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_instanceof_Response_e91b7eb7c611a9ae = function() { return logError(function (arg0) {
let result;
try {
result = getObject(arg0) instanceof Response;
} catch (_) {
result = false;
}
const ret = result;
_assertBoolean(ret);
return ret;
}, arguments) };
imports.wbg.__wbg_url_1bf85c8abeb8c92d = function() { return logError(function (arg0, arg1) {
const ret = getObject(arg1).url;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
}, arguments) };
imports.wbg.__wbg_status_ae8de515694c5c7c = function() { return logError(function (arg0) {
const ret = getObject(arg0).status;
_assertNum(ret);
return ret;
}, arguments) };
imports.wbg.__wbg_headers_5e283e8345689121 = function() { return logError(function (arg0) {
const ret = getObject(arg0).headers;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_new_a220cf903aa02ca2 = function() { return logError(function () {
const ret = new Array();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_push_37c89022f34c01ca = function() { return logError(function (arg0, arg1) {
const ret = getObject(arg0).push(getObject(arg1));
_assertNum(ret);
return ret;
}, arguments) };
imports.wbg.__wbg_instanceof_ArrayBuffer_61dfc3198373c902 = function() { return logError(function (arg0) {
let result;
try {
result = getObject(arg0) instanceof ArrayBuffer;
} catch (_) {
result = false;
}
const ret = result;
_assertBoolean(ret);
return ret;
}, arguments) };
imports.wbg.__wbg_new_796382978dfd4fb0 = function() { return logError(function (arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_newnoargs_76313bd6ff35d0f2 = function() { return logError(function (arg0, arg1) {
const ret = new Function(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_call_1084a111329e68ce = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).call(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_call_89af060b4e1523f2 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = getObject(arg0).call(getObject(arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_next_f9cb570345655b9a = function() { return handleError(function (arg0) {
const ret = getObject(arg0).next();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_next_de3e9db4440638b2 = function() { return logError(function (arg0) {
const ret = getObject(arg0).next;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_done_bfda7aa8f252b39f = function() { return logError(function (arg0) {
const ret = getObject(arg0).done;
_assertBoolean(ret);
return ret;
}, arguments) };
imports.wbg.__wbg_value_6d39332ab4788d86 = function() { return logError(function (arg0) {
const ret = getObject(arg0).value;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_new_525245e2b9901204 = function() { return logError(function () {
const ret = new Object();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_iterator_888179a48810a9fe = function() { return logError(function () {
const ret = Symbol.iterator;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_new_b85e72ed1bfd57f9 = function() { return logError(function (arg0, arg1) {
try {
var state0 = {a: arg0, b: arg1};
var cb0 = (arg0, arg1) => {
const a = state0.a;
state0.a = 0;
try {
return __wbg_adapter_123(a, state0.b, arg0, arg1);
} finally {
state0.a = a;
}
};
const ret = new Promise(cb0);
return addHeapObject(ret);
} finally {
state0.a = state0.b = 0;
}
}, arguments) };
imports.wbg.__wbg_resolve_570458cb99d56a43 = function() { return logError(function (arg0) {
const ret = Promise.resolve(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_then_95e6edc0f89b73b1 = function() { return logError(function (arg0, arg1) {
const ret = getObject(arg0).then(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_then_876bb3c633745cc6 = function() { return logError(function (arg0, arg1, arg2) {
const ret = getObject(arg0).then(getObject(arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_globalThis_86b222e13bdf32ed = function() { return handleError(function () {
const ret = globalThis.globalThis;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_self_3093d5d1f7bcb682 = function() { return handleError(function () {
const ret = self.self;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_window_3bcfc4d31bc012f8 = function() { return handleError(function () {
const ret = window.window;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_global_e5a3fe56f8be9485 = function() { return handleError(function () {
const ret = global.global;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_instanceof_Uint8Array_247a91427532499e = function() { return logError(function (arg0) {
let result;
try {
result = getObject(arg0) instanceof Uint8Array;
} catch (_) {
result = false;
}
const ret = result;
_assertBoolean(ret);
return ret;
}, arguments) };
imports.wbg.__wbg_new_ea1883e1e5e86686 = function() { return logError(function (arg0) {
const ret = new Uint8Array(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_newwithbyteoffsetandlength_8a2cb9ca96b27ec9 = function() { return logError(function (arg0, arg1, arg2) {
const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0);
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_length_8339fcf5d8ecd12e = function() { return logError(function (arg0) {
const ret = getObject(arg0).length;
_assertNum(ret);
return ret;
}, arguments) };
imports.wbg.__wbg_set_d1e79e2388520f18 = function() { return logError(function (arg0, arg1, arg2) {
getObject(arg0).set(getObject(arg1), arg2 >>> 0);
}, arguments) };
imports.wbg.__wbg_buffer_b7b08af79b0b0974 = function() { return logError(function (arg0) {
const ret = getObject(arg0).buffer;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_stringify_bbf45426c92a6bf5 = function() { return handleError(function (arg0) {
const ret = JSON.stringify(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_224d16597dbbfd96 = function() { return handleError(function (arg0, arg1) {
const ret = Reflect.get(getObject(arg0), getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_has_4bfbc01db38743f7 = function() { return handleError(function (arg0, arg1) {
const ret = Reflect.has(getObject(arg0), getObject(arg1));
_assertBoolean(ret);
return ret;
}, arguments) };
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
const ret = debugString(getObject(arg1));
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbindgen_memory = function() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper1265 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 109, __wbg_adapter_36);
return addHeapObject(ret);
}, arguments) };
return imports;
}
function __wbg_init_memory(imports, memory) {
}
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedDataViewMemory0 = null;
cachedUint8ArrayMemory0 = null;
return wasm;
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (typeof module !== 'undefined' && Object.getPrototypeOf(module) === Object.prototype)
({module} = module)
else
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
const imports = __wbg_get_imports();
__wbg_init_memory(imports);
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (typeof module_or_path !== 'undefined' && Object.getPrototypeOf(module_or_path) === Object.prototype)
({module_or_path} = module_or_path)
else
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
if (typeof module_or_path === 'undefined') {
module_or_path = new URL('wasm_mod_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
__wbg_init_memory(imports);
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync };
export default __wbg_init;

Binary file not shown.

58
ext/manifest_cr.json Normal file
View File

@ -0,0 +1,58 @@
{
"manifest_version": 3,
"name": "Teletweet",
"short_name": "Teletweet",
"description": "Post your likes from Twitter (\"\"\"X\"\"\") to your Telegram channel",
"version": "0.0.1",
"author": "@yk",
"minimum_chrome_version": "110",
"offline_enabled": false,
"action": {
"default_title": "Posts a like",
"default_icon": {
"16": "assets/icon16.png",
"32": "assets/icon32.png",
"48": "assets/icon48.png",
"128": "assets/icon128.png"
},
"default_popup": "popup.html",
"show_matches": [
"*://x.com/*"
]
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
},
"background": {
"type": "module",
"service_worker": "js/background.js"
},
"permissions": [
"webRequest",
"storage",
"activeTab",
"offscreen"
],
"host_permissions": [
"*://*.x.com/*",
"<all_urls>"
],
"web_accessible_resources": [
{
"matches": [
"<all_urls>"
],
"resources": [
"js/wasm/wasm_mod.js",
"js/wasm/wasm_mod_bg.wasm"
]
}
],
"content_scripts": [
{
"matches": ["https://*.x.com/*"],
"js": ["js/content-script.js"]
}
]
}

58
ext/manifest_ff.json Normal file
View File

@ -0,0 +1,58 @@
{
"manifest_version": 3,
"name": "Teletweet",
"short_name": "Teletweet",
"description": "Post your likes from Twitter (\"\"\"X\"\"\") to your Telegram channel",
"version": "0.0.1",
"author": "@yk",
"action": {
"default_title": "Posts a like",
"default_icon": {
"16": "assets/icon16.png",
"32": "assets/icon32.png",
"48": "assets/icon48.png",
"128": "assets/icon128.png"
},
"default_popup": "popup.html"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
},
"background": {
"type": "module",
"scripts": ["js/background.js"]
},
"permissions": [
"webRequest",
"storage",
"activeTab",
"offscreen"
],
"host_permissions": [
"*://*.x.com/*",
"<all_urls>"
],
"web_accessible_resources": [
{
"matches": [
"<all_urls>"
],
"resources": [
"js/wasm/wasm_mod.js",
"js/wasm/wasm_mod_bg.wasm"
]
}
],
"browser_specific_settings": {
"gecko": {
"id": "{aed20410-8c19-4833-aa70-42223e29c364}",
"strict_min_version": "112.0"
}
},
"content_scripts": [
{
"matches": ["https://*.x.com/*"],
"js": ["js/content-script.js"]
}
]
}

273
ext/popup.html Normal file
View File

@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="en">
<script type="module" src="js/popup.js"> </script>
<meta charset="UTF-8">
<style>
@font-face {
font-family: "Inner";
src:
local("Inner Tight"),
url("assets/InterTight-VariableFont_wght.ttf") format("truetype"),
url("assets/InterTight-Italic-VariableFont_wght.ttf") format("truetype"),
url("assets/static/InterTight-Medium.ttf") format("truetype")
;
}
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, section, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
:root {
font-size: 1.5em;
}
input, ::placeholder {
font-family: "Inner", sans-serif;
font-weight: 300;
}
body {
font-family: "Inner", sans-serif;
background-color:#000;
background-image:
radial-gradient(at 76% 92%, hsla(215,53%,18%,1) 0px, transparent 50%),
radial-gradient(at 35% 70%, hsla(176,53%,18%,1) 0px, transparent 50%),
radial-gradient(at 73% 70%, hsla(72,53%,18%,1) 0px, transparent 50%),
radial-gradient(at 22% 82%, hsla(203,53%,18%,1) 0px, transparent 50%),
radial-gradient(at 14% 36%, hsla(229,53%,18%,1) 0px, transparent 50%),
radial-gradient(at 19% 58%, hsla(112,53%,18%,1) 0px, transparent 50%);
color: white;
}
#app {
margin: 10px;
padding: 15px;
background: rgba(0, 0, 0, .88);
border-radius: 2px;
min-width: 600px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
}
a, a:visited {
color: rgba(220, 220, 230, 1);
text-decoration: underline;
}
a:hover {
color: rgba(255, 255, 255, 1);
text-decoration: none;
}
#app > section {
width: 49%;
display: flex;
flex-direction: column;
}
#app > section > span {
margin-bottom: 5px;
font-size: 0.77em;
}
input[type=text], input[type=number] {
height: 24px;
font-size: 13px;
padding: 2px 10px;
background: rgba(244, 244, 244, .85);
outline: none;
border: none;
}
input[type=checkbox] {
width: 16px;
height: 16px;
}
h2 {
width: 100%;
text-transform: lowercase;
font-size: 1.5em;
}
#app > section:nth-child(odd) {
text-align: right;
}
#app > * {
margin-bottom: 12px;
}
section > h4 {
font-size: .667em;
margin-top: 11px;
margin-bottom: 5px;
font-weight: 555;
}
section > h5 {
font-size: .50em;
margin-bottom: 5px;
font-weight: 300;
padding-left: 5px;
}
#app > section:nth-child(odd) > h5 {
padding-right: 5px;
}
.controls {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.controls h3 {
font-size: .667em;
font-weight: 555;
}
.control-section {
padding: 10px;
display: flex;
flex-direction: column;
width: 49.5%;
}
.control-section:first-of-type {
border-right: solid 1px rgba(255, 255, 255, .887);
}
.control-section:first-of-type h3 {
text-align: right;
}
.control-section:last-of-type section {
flex-direction: row-reverse;
}
.control-section span {
font-size: .54em;
display: block;
text-align: right;
}
.control-section section {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
footer {
font-size: 0.77em;
}
</style>
<body>
<div id="app">
<h2>↓ Texho</h2>
<section>
<h4>Токен бота</h4>
<h5>(брать здесь: <a target="_blank" href="https://t.me/BotFather">t.me/BotFather</a>)</h5>
<input type="text" id="token" placeholder="your telegram bot token">
</section>
<section>
<h4>ID группы/чата</h4>
<h5>(ID каналов и групп начинаются с -100)</h5>
<input type="text" id="group" placeholder="your group/channel id">
</section>
<section>
<h4>Кастомный домен</h4>
<h5>(дефолт — x.com, можно использовать зеркала с пофикшенными превью, типа fixupx.com (никаких гарантий)</h5>
<input type="text" id="domain" placeholder="domain">
</section>
<section>
<h4>Заголовок</h4>
<h5>Будет отображаться перед ссылкой на твит. Можно использовать теги {me} и {notme}.</h5>
<input type="text" id="header" placeholder="your custom header message">
</section>
<section>
<h4>Фильтр пользователей</h4>
<h5>(без @, через запятую, лайки аккаунтам с юзернеймами из списка не будут отправлены боту)</h5>
<input type="text" id="blacklist" placeholder="(ex.: elonmusk,elonmusksmom,elonmusksgrandmother,elonmusksdad,elonmusksdog)">
</section>
<!-- <section> -->
<!-- <h4>Задержка для скриншота</h4> -->
<!-- <h5>(если включена функция скриншота; мс; 0-300; дефолт 0)</h5> -->
<!-- <input type="number" id="delay" min="0" max="300" placeholder="delay"> -->
<!-- </section> -->
<div class="controls">
<div class="control-section">
<h3>Посты</h3>
<section><input type="checkbox" id="posts"><span>Включить</span></section>
</div>
<div class="control-section">
<h3>Лайки</h3>
<section><input type="checkbox" id="likes"><span>Включить</span></section>
<section><input type="checkbox" id="private"><span>Включая закрытки</span></section>
<section><input type="checkbox" id="screenshot"><span>Делать скриншот (experimental)</span></section>
</div>
</div>
<footer>// <a target="_blank" href="https://t.me/fedpostltd">yk</a> (<a target="_blank" href="https://x.com/ymkeller">x/twitter</a>)</footer>
</div>
</body>
</html>

BIN
firefox.zip Normal file

Binary file not shown.

1443
rs/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

48
rs/Cargo.toml Normal file
View File

@ -0,0 +1,48 @@
[package]
name = "extgk"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
js-sys = "0.3.70"
wasm-bindgen = "0.2.93"
wasm-bindgen-futures = "0.4"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"
urlencoding = "2.1"
# rand only works for WASM if JS support is enabled in a dependency
# See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
rand = "0.8"
getrandom = { version = "0.2", features = ["js"] }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
wee_alloc = { version = "0.4.5", optional = true }
reqwest = { version = "0.12.7", features = ["multipart"] }
[dependencies.web-sys]
version = "0.3.70"
features = [
"console",
"Blob",
"WorkerGlobalScope",
"Window",
"KeyEvent",
"Event",
'Headers',
'Request',
'RequestInit',
'RequestMode',
'Response',
'Storage',
]

198
rs/src/lib.rs Normal file
View File

@ -0,0 +1,198 @@
#![feature(let_chains)]
use std::collections::HashSet;
use serde::{ Deserialize, Serialize };
use serde_json::json;
use wasm_bindgen::{ prelude::wasm_bindgen, JsValue };
use reqwest::{ Client, Url, multipart };
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace=console)]
fn log (s: &str);
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(catch, js_namespace = ["chrome", "storage", "local"], js_name = set)]
async fn storage_set (data: &JsValue) -> Result<JsValue, JsValue>;
#[wasm_bindgen(catch, js_namespace = ["chrome", "storage", "local"], js_name = get)]
async fn storage_get (data: &JsValue) -> Result<JsValue, JsValue>;
}
#[wasm_bindgen]
pub fn post (link: &str) {
}
#[allow(dead_code)]
#[derive(Serialize, Deserialize, Debug)]
struct LocalStorage {
pub token: String,
pub group: String,
#[serde(default)]
pub header: String,
#[serde(default = "default_domain")]
pub domain: String,
#[serde(default)]
pub blacklist: String,
#[serde(skip)]
pub delay: u8,
#[serde(default)]
pub posts: bool,
#[serde(default)]
pub likes: bool,
#[serde(default)]
pub private: bool,
#[serde(default)]
pub screenshot: bool,
}
#[derive(Serialize, Deserialize, Debug)]
struct LikedTweet {
pub id: String,
pub by: TwitterUser,
pub me: Option<TwitterUser>,
#[serde(default)]
pub is_private: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TwitterUser {
pub name: Option<String>,
pub username: String,
}
impl TwitterUser {
fn format (&self) -> String {
match &self.name {
Some(name) => format!("{name} ({})", self.username.trim_start_matches("@")),
None => format!("{}", self.username)
}
}
}
pub fn default_domain () -> String {
String::from("x.com")
}
#[allow(non_upper_case_globals)]
pub fn escape (mut line: String) -> String {
const chars: [char; 21] = [ '\\', '_', '*', '[', ']', '(', ')', '~', '`', '>', '<', '&', '#', '+', '-', '=', '|', '{', '}', '.', '!' ];
for char in chars {
line = line.replace(char, &format!("\\{}", char));
}
line
}
pub fn format_telegram_api_url (token: &str) -> String {
format!("https://api.telegram.org/bot{token}")
}
pub fn format_tweet_url (domain: &str, username: &str, id: &str) -> String {
format!("https://{domain}/{username}/status/{id}")
}
pub fn format_caption (url: &str, header: &str, by: &TwitterUser, me: &Option<TwitterUser>) -> String {
let by = by.format();
let me = me.as_ref().map(TwitterUser::format);
// @TODO: Edge case of where header is set but we can't retrieve Me
let header = escape(header
.replace("{me}", &me.unwrap_or_default())
.replace("{notme}", &by));
format!("{}{header}{}{}",
if !header.is_empty() { "`" } else { Default::default() },
if !header.is_empty() { "`\r\n\r\n" } else { Default::default() },
escape(url.to_owned())
)
}
#[allow(unused_macros)]
macro_rules! log {
($data: expr) => {
log(&format!("{:?}", $data))
};
}
#[wasm_bindgen]
pub async fn like (tweet: JsValue, image: Option<Vec<u8>>) -> Option<u8> {
let store = storage_get(&JsValue::null()).await.ok()?;
let store: LocalStorage = serde_wasm_bindgen::from_value(store).ok()?;
if !store.likes { return None; }
let blacklist = store.blacklist.split(",").map(str::trim).collect::<HashSet<&str>>();
let tweet: LikedTweet = serde_wasm_bindgen::from_value(tweet).ok()?;
if !store.private && tweet.is_private { return None; }
if blacklist.contains(tweet.by.username.as_str()) { return None; }
let domain = if store.domain.is_empty() { default_domain() } else { store.domain };
let group = store.group;
let token = store.token;
let url = format_tweet_url(&domain, &tweet.by.username, &tweet.id);
let caption = format_caption(&url, &store.header, &tweet.by, &tweet.me);
let client = Client::new();
let link_preview_options = json!({
"is_disabled": false,
"url": url,
"prefer_large_media": true,
"show_above_text": false,
}).to_string();
let (form, endpoint) = if let Some(image) = image && store.screenshot {
let endpoint = format!("{}/sendPhoto", format_telegram_api_url(&token));
let part = multipart::Part::bytes(image).file_name("test.png").mime_str("image/png").ok()?;
(reqwest::multipart::Form::new()
.text("chat_id", group)
.text("caption", caption)
.text("parse_mode", "MarkdownV2")
.text("link_preview_options", link_preview_options)
.part("photo", part),
endpoint)
} else {
let endpoint = format!("{}/sendMessage", format_telegram_api_url(&token));
(reqwest::multipart::Form::new()
.text("chat_id", group)
.text("link_preview_options", link_preview_options)
.text("parse_mode", "MarkdownV2")
.text("text", caption),
endpoint)
};
let endpoint = Url::parse(&endpoint).ok()?;
let _resp = client
.post(endpoint)
.multipart(form)
.send().await.ok()?;
log!(_resp);
Some(0)
}