RedditSubmitPrefillScript.swift 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import Foundation
  2. enum RedditSubmitPrefillScript {
  3. static func make(prefill: RedditSubmitPrefill) -> String {
  4. let payload: [String: Any] = [
  5. "body": prefill.body,
  6. "pollOptions": prefill.pollOptions,
  7. "pollDurationDays": prefill.pollDurationDays,
  8. ]
  9. guard
  10. let jsonData = try? JSONSerialization.data(withJSONObject: payload),
  11. let jsonString = String(data: jsonData, encoding: .utf8)
  12. else {
  13. return ""
  14. }
  15. return """
  16. (function() {
  17. const config = \(jsonString);
  18. const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  19. const queryAllDeep = (selector, root = document) => {
  20. const results = [];
  21. const visit = (node) => {
  22. results.push(...Array.from(node.querySelectorAll(selector)));
  23. Array.from(node.querySelectorAll('*')).forEach((element) => {
  24. if (element.shadowRoot) {
  25. visit(element.shadowRoot);
  26. }
  27. });
  28. };
  29. visit(root);
  30. return results;
  31. };
  32. const clickPollTab = () => {
  33. const candidates = queryAllDeep('button, [role="tab"], [role="radio"], label, a, div[role="button"]');
  34. const pollControl = candidates.find((element) => {
  35. const text = (element.textContent || '').trim().toLowerCase();
  36. const label = (element.getAttribute('aria-label') || '').trim().toLowerCase();
  37. return text === 'poll' || label === 'poll' || label.includes('poll post');
  38. });
  39. if (!pollControl) {
  40. return false;
  41. }
  42. pollControl.click();
  43. return true;
  44. };
  45. const setFieldValue = (field, value) => {
  46. if (!field || value == null) {
  47. return;
  48. }
  49. if (field.tagName === 'TEXTAREA' || field.tagName === 'INPUT') {
  50. field.value = value;
  51. } else if (field.isContentEditable) {
  52. field.textContent = value;
  53. } else {
  54. return;
  55. }
  56. field.dispatchEvent(new Event('input', { bubbles: true }));
  57. field.dispatchEvent(new Event('change', { bubbles: true }));
  58. };
  59. const fillBody = () => {
  60. if (!config.body) {
  61. return;
  62. }
  63. const fields = queryAllDeep('textarea, div[contenteditable="true"], faceplate-textarea-input textarea, [data-testid*="body"] textarea');
  64. const bodyField = fields.find((field) => {
  65. const placeholder = (field.getAttribute('placeholder') || '').toLowerCase();
  66. const aria = (field.getAttribute('aria-label') || '').toLowerCase();
  67. return placeholder.includes('body') || placeholder.includes('text') || aria.includes('body') || aria.includes('optional');
  68. }) || fields[0];
  69. if (bodyField) {
  70. setFieldValue(bodyField, config.body);
  71. }
  72. };
  73. const pollOptionFields = () => {
  74. return queryAllDeep('input[type="text"], textarea').filter((field) => {
  75. const placeholder = (field.getAttribute('placeholder') || '').toLowerCase();
  76. const aria = (field.getAttribute('aria-label') || '').toLowerCase();
  77. const name = (field.getAttribute('name') || '').toLowerCase();
  78. const testId = (field.getAttribute('data-testid') || '').toLowerCase();
  79. return placeholder.includes('option')
  80. || aria.includes('option')
  81. || name.includes('poll')
  82. || testId.includes('poll-option');
  83. });
  84. };
  85. const clickAddOption = () => {
  86. const buttons = queryAllDeep('button, [role="button"]');
  87. const addButton = buttons.find((button) => /add.*option/i.test((button.textContent || '').trim()));
  88. if (addButton) {
  89. addButton.click();
  90. return true;
  91. }
  92. return false;
  93. };
  94. const fillPollOptions = async () => {
  95. for (let index = 0; index < config.pollOptions.length; index += 1) {
  96. let fields = pollOptionFields();
  97. let attempts = 0;
  98. while (fields.length <= index && attempts < 4) {
  99. if (!clickAddOption()) {
  100. break;
  101. }
  102. await sleep(200);
  103. fields = pollOptionFields();
  104. attempts += 1;
  105. }
  106. if (fields[index]) {
  107. setFieldValue(fields[index], config.pollOptions[index]);
  108. }
  109. }
  110. };
  111. const selectDuration = () => {
  112. const label = config.pollDurationDays === 1
  113. ? '1 day'
  114. : `${config.pollDurationDays} days`;
  115. const controls = queryAllDeep('button, [role="radio"], label, option, span, div[role="button"]');
  116. const match = controls.find((control) => {
  117. const text = (control.textContent || '').trim().toLowerCase();
  118. return text === label || text.startsWith(`${config.pollDurationDays} day`);
  119. });
  120. if (match) {
  121. match.click();
  122. }
  123. };
  124. const applyPollPrefill = async () => {
  125. if (!clickPollTab()) {
  126. return false;
  127. }
  128. await sleep(500);
  129. fillBody();
  130. await fillPollOptions();
  131. selectDuration();
  132. return true;
  133. };
  134. return applyPollPrefill();
  135. })();
  136. """
  137. }
  138. }