|
|
@@ -0,0 +1,159 @@
|
|
|
+import Foundation
|
|
|
+
|
|
|
+enum RedditSubmitPrefillScript {
|
|
|
+ static func make(prefill: RedditSubmitPrefill) -> String {
|
|
|
+ let payload: [String: Any] = [
|
|
|
+ "body": prefill.body,
|
|
|
+ "pollOptions": prefill.pollOptions,
|
|
|
+ "pollDurationDays": prefill.pollDurationDays,
|
|
|
+ ]
|
|
|
+
|
|
|
+ guard
|
|
|
+ let jsonData = try? JSONSerialization.data(withJSONObject: payload),
|
|
|
+ let jsonString = String(data: jsonData, encoding: .utf8)
|
|
|
+ else {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+
|
|
|
+ return """
|
|
|
+ (function() {
|
|
|
+ const config = \(jsonString);
|
|
|
+
|
|
|
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
+
|
|
|
+ const queryAllDeep = (selector, root = document) => {
|
|
|
+ const results = [];
|
|
|
+ const visit = (node) => {
|
|
|
+ results.push(...Array.from(node.querySelectorAll(selector)));
|
|
|
+ Array.from(node.querySelectorAll('*')).forEach((element) => {
|
|
|
+ if (element.shadowRoot) {
|
|
|
+ visit(element.shadowRoot);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+ visit(root);
|
|
|
+ return results;
|
|
|
+ };
|
|
|
+
|
|
|
+ const clickPollTab = () => {
|
|
|
+ const candidates = queryAllDeep('button, [role="tab"], [role="radio"], label, a, div[role="button"]');
|
|
|
+ const pollControl = candidates.find((element) => {
|
|
|
+ const text = (element.textContent || '').trim().toLowerCase();
|
|
|
+ const label = (element.getAttribute('aria-label') || '').trim().toLowerCase();
|
|
|
+ return text === 'poll' || label === 'poll' || label.includes('poll post');
|
|
|
+ });
|
|
|
+ if (!pollControl) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ pollControl.click();
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ const setFieldValue = (field, value) => {
|
|
|
+ if (!field || value == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (field.tagName === 'TEXTAREA' || field.tagName === 'INPUT') {
|
|
|
+ field.value = value;
|
|
|
+ } else if (field.isContentEditable) {
|
|
|
+ field.textContent = value;
|
|
|
+ } else {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
+ field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
+ };
|
|
|
+
|
|
|
+ const fillBody = () => {
|
|
|
+ if (!config.body) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const fields = queryAllDeep('textarea, div[contenteditable="true"], faceplate-textarea-input textarea, [data-testid*="body"] textarea');
|
|
|
+ const bodyField = fields.find((field) => {
|
|
|
+ const placeholder = (field.getAttribute('placeholder') || '').toLowerCase();
|
|
|
+ const aria = (field.getAttribute('aria-label') || '').toLowerCase();
|
|
|
+ return placeholder.includes('body') || placeholder.includes('text') || aria.includes('body') || aria.includes('optional');
|
|
|
+ }) || fields[0];
|
|
|
+
|
|
|
+ if (bodyField) {
|
|
|
+ setFieldValue(bodyField, config.body);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const pollOptionFields = () => {
|
|
|
+ return queryAllDeep('input[type="text"], textarea').filter((field) => {
|
|
|
+ const placeholder = (field.getAttribute('placeholder') || '').toLowerCase();
|
|
|
+ const aria = (field.getAttribute('aria-label') || '').toLowerCase();
|
|
|
+ const name = (field.getAttribute('name') || '').toLowerCase();
|
|
|
+ const testId = (field.getAttribute('data-testid') || '').toLowerCase();
|
|
|
+ return placeholder.includes('option')
|
|
|
+ || aria.includes('option')
|
|
|
+ || name.includes('poll')
|
|
|
+ || testId.includes('poll-option');
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const clickAddOption = () => {
|
|
|
+ const buttons = queryAllDeep('button, [role="button"]');
|
|
|
+ const addButton = buttons.find((button) => /add.*option/i.test((button.textContent || '').trim()));
|
|
|
+ if (addButton) {
|
|
|
+ addButton.click();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ };
|
|
|
+
|
|
|
+ const fillPollOptions = async () => {
|
|
|
+ for (let index = 0; index < config.pollOptions.length; index += 1) {
|
|
|
+ let fields = pollOptionFields();
|
|
|
+ let attempts = 0;
|
|
|
+
|
|
|
+ while (fields.length <= index && attempts < 4) {
|
|
|
+ if (!clickAddOption()) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ await sleep(200);
|
|
|
+ fields = pollOptionFields();
|
|
|
+ attempts += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fields[index]) {
|
|
|
+ setFieldValue(fields[index], config.pollOptions[index]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const selectDuration = () => {
|
|
|
+ const label = config.pollDurationDays === 1
|
|
|
+ ? '1 day'
|
|
|
+ : `${config.pollDurationDays} days`;
|
|
|
+
|
|
|
+ const controls = queryAllDeep('button, [role="radio"], label, option, span, div[role="button"]');
|
|
|
+ const match = controls.find((control) => {
|
|
|
+ const text = (control.textContent || '').trim().toLowerCase();
|
|
|
+ return text === label || text.startsWith(`${config.pollDurationDays} day`);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (match) {
|
|
|
+ match.click();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const applyPollPrefill = async () => {
|
|
|
+ if (!clickPollTab()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ await sleep(500);
|
|
|
+ fillBody();
|
|
|
+ await fillPollOptions();
|
|
|
+ selectDuration();
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ return applyPollPrefill();
|
|
|
+ })();
|
|
|
+ """
|
|
|
+ }
|
|
|
+}
|