Camamba Forums Search Library

fetches forums, threads and posts

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/446112/1060781/Camamba%20Forums%20Search%20Library.js

  1. // ==UserScript==
  2. // @name Camamba Forums Search Library
  3. // @namespace hoehleg.userscripts.private
  4. // @version 0.0.3
  5. // @description fetches forums, threads and posts
  6. // @author Gerrit Höhle
  7. // @license MIT
  8. //
  9. // @require https://greatest.deepsurf.us/scripts/405144-httprequest/code/HttpRequest.js?version=1060129
  10. //
  11. // @grant GM_xmlhttpRequest
  12. //
  13. // ==/UserScript==
  14.  
  15. /* jslint esversion: 9 */
  16.  
  17. /**
  18. * @typedef ForumDef
  19. * @property {number} id
  20. * @property {string} lng
  21. * @property {string} title
  22. */
  23.  
  24. /**
  25. * @typedef ThreadDef
  26. * @property {string} title
  27. * @property {number} id
  28. * @property {number} forumId
  29. * @property {number} postCount
  30. */
  31.  
  32. /**
  33. * @typedef PostDef
  34. * @property {number} forumId
  35. * @property {number} threadId
  36. * @property {number} page
  37. * @property {number} id
  38. * @property {Date} postDate
  39. * @property {string} text
  40. * @property {string} uname
  41. * @property {number} uid
  42. */
  43.  
  44. /**
  45. * @typedef ThreadIdentifier
  46. * @property {number} forumId
  47. * @property {number} threadId
  48. */
  49.  
  50. class Post {
  51. /**
  52. * @param {PostDef} param0
  53. */
  54. constructor({ forumId, threadId, page, id, postDate, text, uname, uid }) {
  55. /** @type {number} */
  56. this.forumId = forumId;
  57. /** @type {number} */
  58. this.threadId = threadId;
  59. /** @type {number} */
  60. this.page = page;
  61. /** @type {number} */
  62. this.id = id;
  63. /** @type {Date} */
  64. this.postDate = postDate;
  65. /** @type {string} */
  66. this.text = text;
  67. /** @type {string} */
  68. this.uname = uname;
  69. /** @type {number} */
  70. this.uid = uid;
  71. }
  72.  
  73. async delete() {
  74. return await HttpRequest.send({
  75. method: 'GET', url: "https://www.camamba.com/forum_view.php", params: {
  76. thread: this.threadId,
  77. forum: this.forumId,
  78. delete: this.id,
  79. page: this.page,
  80. }
  81. });
  82. }
  83. }
  84.  
  85. class Thread {
  86.  
  87. /**
  88. * @param {ThreadDef} param0
  89. */
  90. constructor({ title, id, forumId, postCount }) {
  91. /** @type {string} */
  92. this.title = title;
  93. /** @type {number} */
  94. this.id = id;
  95. /** @type {number} */
  96. this.forumId = forumId;
  97. /** @type {number} */
  98. this.postCount = postCount;
  99. }
  100.  
  101. /**
  102. * @returns {Promise<Array<Post>>}
  103. */
  104. async getPosts(resultSizeLimit = 0) {
  105. return Thread.getPosts({ threadId: this.id, forumId: this.forumId, resultSizeLimit });
  106. }
  107.  
  108. async delete() {
  109. for (const post of await this.getPosts()) {
  110. post.delete();
  111. }
  112. }
  113.  
  114. /**
  115. * @param {ThreadIdentifier} param0
  116. * @returns {Promise<Array<Post>>}
  117. */
  118. static async getPosts({ threadId, forumId, resultSizeLimit = 0 }) {
  119. return (await HttpRequestHtml.send({
  120. url: "https://www.camamba.com/forum_view.php",
  121. params: {
  122. thread: threadId,
  123. forum: forumId,
  124. },
  125. pageNr: 1,
  126. pagesMaxCount: resultSizeLimit ? Math.ceil(resultSizeLimit / 10) : 10000,
  127. paramsConfiguratorForPageNr: (params, pageNr) => ({ ...params, page: pageNr - 1 }),
  128. hasNextPage: (res, _req) => res.html.querySelectorAll("td.psmallbox").length >= 30,
  129. resultTransformer: (res, req) => {
  130.  
  131. return [...res.html.querySelectorAll("td.psmallbox")].map(td => {
  132. let postId = null, text = "", postDate = null;
  133.  
  134. const div = td.querySelector("div");
  135. if (div) {
  136. const deleteLink = div.querySelector('a[href^="javascript:deletePost"]');
  137.  
  138. if (deleteLink) {
  139. const postIdMatch = /(?<=javascript:deletePost)\d+(?=\(\))/.exec(deleteLink.href);
  140. postId = postIdMatch === null ? null : Number.parseInt(postIdMatch[0]);
  141. }
  142.  
  143. text = [...div.childNodes]
  144. .filter(el => el.tagName !== "SCRIPT")
  145. .filter(el => el.tagName !== "A" || (!["javascript:deletePost", "javascript:parent.location", "javascript:var noop="].some(s => el.href.startsWith(s))))
  146. .map(el => el.textContent).join("").trim();
  147. }
  148.  
  149. let uname = "", uid = null;
  150.  
  151. const tdLeft = td.previousElementSibling;
  152. if (tdLeft && [...tdLeft.classList].includes("psmallbox2")) {
  153. const linkOpenProfile = tdLeft.querySelector('a[href^="javascript:openProfile("]');
  154. if (linkOpenProfile) {
  155. const unameMatch = /(?<=javascript:openProfile\(['"])\w+(?=['"]\))/.exec(linkOpenProfile.href);
  156. if (unameMatch) {
  157. uname = unameMatch[0];
  158. }
  159. }
  160.  
  161. const imgUserpic = tdLeft.querySelector('img[src^="/userpics"]');
  162. if (imgUserpic) {
  163. const uidMatch = /(?<=userpics\/)\d+(?=\.s\.jpg)/.exec(imgUserpic.src);
  164. if (uidMatch) {
  165. uid = uidMatch[0];
  166. }
  167. }
  168.  
  169. const divDate = tdLeft.querySelector('div.smalltext');
  170. if (divDate) {
  171. const postDateMatch = /(\d{2}).(\d{2}).(\d{4})\s(\d{1,2}):(\d{2}):(\d{2})/.exec(divDate.innerText.trim());
  172. if (postDateMatch) {
  173. const day = postDateMatch[1];
  174. const month = postDateMatch[2];
  175. const year = postDateMatch[3];
  176. const hour = postDateMatch[4];
  177. const minute = postDateMatch[5];
  178. const second = postDateMatch[6];
  179. postDate = new Date(year, month - 1, day, hour, minute, second);
  180. }
  181. }
  182. }
  183.  
  184. let page = req.params.page;
  185. return { forumId, threadId, page, id: postId, postDate, text, uname, uid };
  186. }).filter(p => p.id && p.text).map(p => new Post(p));
  187. },
  188. })).flat();
  189. }
  190. }
  191.  
  192. const Forum = (() => {
  193.  
  194. /**
  195. * @param {number} forumId
  196. * @param {number} [resultSizeLimit]
  197. * @returns {HttpRequestHtml}
  198. */
  199. const createRequest = (forumId, resultSizeLimit = 0) => new HttpRequestHtml({
  200. url: "https://www.camamba.com/forum_view.php",
  201. params: { forum: forumId },
  202. pageNr: 1,
  203. pagesMaxCount: resultSizeLimit ? Math.ceil(resultSizeLimit / 10) : 10000,
  204. paramsConfiguratorForPageNr: (params, pageNr) => ({ ...params, page: pageNr - 1 }),
  205. hasNextPage: (res, _req) => res.html.querySelectorAll('a[href^="?thread="]').length >= 10,
  206. resultTransformer: (res, _req) => {
  207. return [...res.html.querySelectorAll('a[href^="?thread="]')].map(a => {
  208.  
  209. const matchThreadId = new RegExp(`(?<=\\?thread=)\\d+(?=&forum=${forumId})`).exec(a.href);
  210. const threadId = matchThreadId === null ? null : Number.parseInt(matchThreadId[0]);
  211.  
  212. let postCount = (() => {
  213.  
  214. let postCountTD = a.parentElement;
  215.  
  216. while (postCountTD !== null && postCountTD.tagName !== 'TD') {
  217. postCountTD = postCountTD.parentElement;
  218. }
  219.  
  220. for (let i = 0; i < 2 && postCountTD !== null; i++) {
  221. postCountTD = postCountTD.nextElementSibling;
  222. }
  223.  
  224. if (postCountTD != null) {
  225. let postCountMatch = /^\d+$/.exec(postCountTD.innerText.trim());
  226. if (postCountMatch && postCountMatch.length) {
  227. return Number.parseInt(postCountMatch[0]);
  228. }
  229. }
  230.  
  231. return 0;
  232. })();
  233.  
  234. return { title: a.innerHTML, id: threadId, forumId, postCount };
  235.  
  236. }).filter(t => t.id && t.forumId && t.postCount).map(t => new Thread(t));
  237. },
  238. });
  239.  
  240. return class Forum {
  241. /** @param {ForumDef} param0 */
  242. constructor({ id, lng, title }) {
  243. /** @type {number} */
  244. this.id = id;
  245. /** @type {string} */
  246. this.lng = lng;
  247. /** @type {string} */
  248. this.title = title;
  249. }
  250.  
  251. /**
  252. * @returns {Promise<Array<Thread>>}
  253. */
  254. async getThreads(resultSizeLimit = 0) {
  255. this.request = createRequest(this.id, resultSizeLimit);
  256. this.lastResults = (await this.request.send()).flat();
  257. return this.lastResults;
  258. }
  259.  
  260. /**
  261. * @returns {Promise<Array<Thread>>}
  262. */
  263. async getNextThreads() {
  264. if (!(this.request && this.lastResults && this.lastResults.length)) {
  265. return [];
  266. }
  267. this.request.pageNr += (this.request.pagesMaxCount);
  268. return (await this.request.send()).flat();
  269. }
  270.  
  271. /**
  272. * @param {number} forumId
  273. * @returns {Promise<Array<Thread>>}
  274. */
  275. static async getThreads(forumId, resultSizeLimit = 0) {
  276. return (await createRequest(forumId, resultSizeLimit).send()).flat();
  277. }
  278. };
  279. })();
  280.  
  281.  
  282.  
  283. const Foren = (() => {
  284.  
  285. /**
  286. * @type {Promise<Array<Forum>>}
  287. */
  288. const foren = HttpRequestHtml.send({
  289. url: 'https://www.camamba.com/forum.php',
  290. params: { mode: 'all' },
  291. resultTransformer: (resp, _req) => [...resp.html.querySelectorAll('a[href^="/forum_view"]')].map(a => {
  292.  
  293. const idMatch = /(?<=forum=)\d{1,2}$/.exec(a.href);
  294. const id = idMatch === null ? null : Number.parseInt(idMatch[0]);
  295.  
  296. const lngMatch = /(?<=^\[)\D\D(?=\])/g.exec(a.textContent);
  297. const lng = lngMatch === null ? null : lngMatch[0].toLowerCase();
  298.  
  299. const titleMatch = /(?<=^\[\D\D\]\s).+$/.exec(a.textContent);
  300. const title = titleMatch === null ? null : titleMatch[0];
  301.  
  302. return { id, lng, title };
  303.  
  304. }).filter(forum => forum.id && forum.lng).map(f => new Forum(f)),
  305. });
  306.  
  307. return {
  308. /**
  309. * @returns {Promise<Array<Forum>>}
  310. */
  311. getAll: async () => await foren,
  312.  
  313. /**
  314. * @param {string} lng
  315. * @returns {Promise<Array<Forum>>}
  316. */
  317. byLanguage: async (lng) => (await foren).filter(f => f.lng.toUpperCase() === lng.toUpperCase()),
  318.  
  319. /**
  320. * @param {number} id
  321. * @returns {Promise<Forum>}
  322. */
  323. byId: async (id) => (await foren).find(f => f.id == id),
  324. };
  325. })();