AdminAutospam.vue 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106
  1. <template>
  2. <div>
  3. <div class="header bg-primary pb-3 mt-n4">
  4. <div class="container-fluid">
  5. <div class="header-body">
  6. <div class="row align-items-center py-4">
  7. <div class="col-xl-4 col-lg-6 col-md-4">
  8. <p class="display-1 text-white d-inline-block mb-0">Autospam</p>
  9. <p class="text-lighter">The automated spam detection system</p>
  10. </div>
  11. <div class="col-xl-4 col-lg-3 col-md-4">
  12. <div class="card card-stats mb-lg-0">
  13. <div class="card-body">
  14. <div class="row">
  15. <div class="col">
  16. <h5 class="card-title text-uppercase text-muted mb-0">Active Autospam</h5>
  17. <span class="h2 font-weight-bold mb-0">{{ formatCount(config.open) }}</span>
  18. </div>
  19. <div class="col-auto">
  20. <div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow">
  21. <i class="far fa-sensor-alert"></i>
  22. </div>
  23. </div>
  24. </div>
  25. </div>
  26. </div>
  27. </div>
  28. <div class="col-xl-4 col-lg-3 col-md-4">
  29. <div class="card card-stats bg-dark mb-lg-0">
  30. <div class="card-body">
  31. <div class="row">
  32. <div class="col">
  33. <h5 class="card-title text-uppercase text-muted mb-0">Closed Autospam</h5>
  34. <span class="h2 font-weight-bold text-muted mb-0">{{ formatCount(config.closed) }}</span>
  35. </div>
  36. <div class="col-auto">
  37. <div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow">
  38. <i class="far fa-shield-alt"></i>
  39. </div>
  40. </div>
  41. </div>
  42. </div>
  43. </div>
  44. </div>
  45. </div>
  46. </div>
  47. </div>
  48. </div>
  49. <div v-if="!loaded" class="my-5 text-center">
  50. <b-spinner />
  51. </div>
  52. <div v-else class="m-n2 m-lg-4">
  53. <div class="container-fluid mt-4">
  54. <div class="row mb-3 justify-content-between">
  55. <div class="col-12">
  56. <ul class="nav nav-pills">
  57. <li class="nav-item">
  58. <button :class="['nav-link', { active: tabIndex == 0}]" @click.prevent="toggleTab(0)">Dashboard</button>
  59. </li>
  60. <li class="nav-item">
  61. <button :class="['nav-link', { active: tabIndex == 'about'}]" @click.prevent="toggleTab('about')">About / How to Use Autospam</button>
  62. </li>
  63. <li class="nav-item">
  64. <button :class="['nav-link', { active: tabIndex == 'train'}]" @click.prevent="toggleTab('train')">Train Autospam</button>
  65. </li>
  66. <li class="nav-item">
  67. <button :class="['nav-link', { active: tabIndex == 'closed_reports'}]" @click.prevent="toggleTab('closed_reports')">Closed Reports</button>
  68. </li>
  69. <li class="nav-item">
  70. <button :class="['nav-link', { active: tabIndex == 'manage_tokens'}]" @click.prevent="toggleTab('manage_tokens')">Manage Tokens</button>
  71. </li>
  72. <li class="nav-item">
  73. <button :class="['nav-link', { active: tabIndex == 'import_export'}]" @click.prevent="toggleTab('import_export')">Import/Export</button>
  74. </li>
  75. </ul>
  76. </div>
  77. </div>
  78. <div v-if="this.tabIndex === 0" class="row">
  79. <div class="col-12 col-md-4">
  80. <div v-if="config.autospam_enabled === null">
  81. </div>
  82. <div v-else-if="config.autospam_enabled" class="card bg-dark" style="min-height: 209px;">
  83. <div class="card-body text-center">
  84. <p><i class="far fa-check-circle fa-5x text-success"></i></p>
  85. <p class="lead text-light mb-0">Autospam Service Operational</p>
  86. </div>
  87. </div>
  88. <div v-else class="card bg-dark" style="min-height: 209px;">
  89. <div class="card-body text-center">
  90. <p><i class="far fa-exclamation-circle fa-5x text-danger"></i></p>
  91. <p class="lead text-danger font-weight-bold mb-0">Autospam Service Inactive</p>
  92. <p class="small text-light mb-0">To activate, <a href="/i/admin/settings">click here</a> and enable <span class="font-weight-bold">Spam detection</span></p>
  93. </div>
  94. </div>
  95. <div v-if="config.nlp_enabled === null">
  96. </div>
  97. <div v-else-if="config.nlp_enabled" class="card bg-dark" style="min-height: 209px;">
  98. <div class="card-body text-center">
  99. <p><i class="far fa-check-circle fa-5x text-success"></i></p>
  100. <p class="lead text-light">Advanced (NLP) Detection Active</p>
  101. <a class="btn btn-outline-danger btn-block font-weight-bold" :class="{ disabled: config.autospam_enabled != true}" href="#" :disabled="config.autospam_enabled != true" @click.prevent="disableAdvanced">Disable Advanced Detection</a>
  102. </div>
  103. </div>
  104. <div v-else class="card bg-dark" style="min-height: 209px;">
  105. <div class="card-body text-center">
  106. <p><i class="far fa-exclamation-circle fa-5x text-danger"></i></p>
  107. <p class="lead text-danger font-weight-bold">Advanced (NLP) Detection Inactive</p>
  108. <a class="btn btn-primary btn-block font-weight-bold" :class="{ disabled: config.autospam_enabled != true}" href="#" :disabled="config.autospam_enabled != true" @click.prevent="enableAdvanced">Enable Advanced Detection</a>
  109. </div>
  110. </div>
  111. </div>
  112. <div class="col-12 col-md-8">
  113. <div class="card bg-default">
  114. <div class="card-header bg-transparent">
  115. <div class="row align-items-center">
  116. <div class="col">
  117. <h6 class="text-light text-uppercase ls-1 mb-1">Stats</h6>
  118. <h5 class="h3 text-white mb-0">Autospam Detections</h5>
  119. </div>
  120. </div>
  121. </div>
  122. <div class="card-body">
  123. <div class="chart">
  124. <canvas id="c1-dark" class="chart-canvas"></canvas>
  125. </div>
  126. </div>
  127. </div>
  128. </div>
  129. </div>
  130. <div v-else-if="this.tabIndex === 'about'">
  131. <div class="row">
  132. <div class="col-12">
  133. <div class="card card-body">
  134. <h1>About Autospam</h1>
  135. <p class="mb-0">To detect and mitigate spam, we built Autospam, an internal tool that uses NLP and other behavioural metrics to classify potential spam posts.</p>
  136. <hr />
  137. <h2>Standard Detection</h2>
  138. <p>Standard or "Classic" detection works by evaluating several "signals" from the post and it's associated account.</p>
  139. <p>Some of the following "signals" may trigger a positive detection from public posts:</p>
  140. <ul>
  141. <li>Account is less than 6 months old</li>
  142. <li>Account has less than 100 followers</li>
  143. <li>Post contains one or more of: <span class="badge badge-primary">https://</span> <span class="badge badge-primary">http://</span> <span class="badge badge-primary">hxxps://</span> <span class="badge badge-primary">hxxp://</span> <span class="badge badge-primary">www.</span> <span class="badge badge-primary">.com</span> <span class="badge badge-primary">.net</span> <span class="badge badge-primary">.org</span> </li>
  144. </ul>
  145. <p>If you've marked atleast one positive detection from an account as <span class="font-weight-bold">Not spam</span>, any future posts they create will skip detection.</p>
  146. <hr />
  147. <h2>Advanced Detection</h2>
  148. <p>Advanced Detection works by using a statistical method that combines prior knowledge and observed data to estimate an average value. It assigns weights to both the prior knowledge and the observed data, allowing for a more informed and reliable estimation that adapts to new information.</p>
  149. <p>When you train Spam or Not Spam data, the caption is broken up into words (tokens) and are counted (weights) and then stored in the appropriate category (Spam or Not Spam).</p>
  150. <p>The training data is then used to classify spam on future posts (captions) by calculating each token and associated weights and comparing it to known categories (Spam or Not Spam).</p>
  151. </div>
  152. </div>
  153. </div>
  154. </div>
  155. <div v-else-if="this.tabIndex === 'train'">
  156. <div class="row">
  157. <div class="col-12">
  158. <div class="card card-body">
  159. <p class="mb-0">
  160. In order for Autospam to be effective, you need to train it by classifying data as spam or not-spam.
  161. </p>
  162. <p class="mb-0 small">
  163. We recommend atleast 200 classifications for both spam and not-spam, it is important to train Autospam on both so you get more accurate results.
  164. </p>
  165. </div>
  166. </div>
  167. </div>
  168. <div class="row">
  169. <div class="col-12 col-md-6">
  170. <div class="card bg-dark">
  171. <div class="card-header bg-gradient-primary text-white font-weight-bold">Train Spam Posts</div>
  172. <div class="card-body">
  173. <div class="d-flex flex-column align-items-center justify-content-center py-4" style="gap:1rem;">
  174. <p class="mb-0">
  175. <i class="far fa-sensor-alert fa-5x text-danger"></i>
  176. </p>
  177. <p class="lead text-lighter">Use existing posts marked as spam to train Autospam</p>
  178. <button
  179. class="btn btn-primary btn-lg font-weight-bold btn-block"
  180. :class="{ disabled: config.files.spam.exists}"
  181. :disabled="config.files.spam.exists"
  182. @click.prevent="autospamTrainSpam">
  183. {{ config.files.spam.exists ? 'Already trained' : 'Train Spam' }}
  184. </button>
  185. </div>
  186. </div>
  187. </div>
  188. </div>
  189. <div class="col-12 col-md-6">
  190. <div class="card bg-dark">
  191. <div class="card-header bg-gradient-primary text-white font-weight-bold">Train Non-Spam Posts</div>
  192. <div class="card-body">
  193. <div class="d-flex flex-column align-items-center justify-content-center py-4" style="gap:1rem;">
  194. <p class="mb-0">
  195. <i class="far fa-check-circle fa-5x text-success"></i>
  196. </p>
  197. <p class="lead text-lighter">Use posts from trusted users to train non-spam posts</p>
  198. <button
  199. class="btn btn-primary btn-lg font-weight-bold btn-block"
  200. :class="{ disabled: config.files.ham.exists}"
  201. :disabled="config.files.ham.exists"
  202. @click.prevent="autospamTrainNonSpam">
  203. {{ config.files.ham.exists ? 'Already trained' : 'Train Non-Spam' }}
  204. </button>
  205. </div>
  206. </div>
  207. </div>
  208. </div>
  209. </div>
  210. </div>
  211. <div v-else-if="this.tabIndex === 'closed_reports'">
  212. <template v-if="closedReportsFetched">
  213. <div class="table-responsive rounded">
  214. <table class="table table-dark">
  215. <thead class="thead-dark">
  216. <tr>
  217. <th scope="col">ID</th>
  218. <th scope="col">Type</th>
  219. <th scope="col">Reported Account</th>
  220. <th scope="col">Created</th>
  221. <th scope="col">View Report</th>
  222. </tr>
  223. </thead>
  224. <tbody>
  225. <tr v-for="(report, idx) in closedReports.data" :key="'closed_reports' + report.id + idx">
  226. <td class="font-weight-bold text-monospace text-muted align-middle">
  227. {{ report.id}}
  228. </td>
  229. <td class="align-middle">
  230. <p class="text-capitalize font-weight-bold mb-0">Autospam Post</p>
  231. </td>
  232. <td class="align-middle">
  233. <a v-if="report.status && report.status.account" :href="`/i/web/profile/${report.status.account.id}`" target="_blank" class="text-white">
  234. <div class="d-flex align-items-center" style="gap:0.61rem;">
  235. <img
  236. :src="report.status.account.avatar"
  237. width="30"
  238. height="30"
  239. style="object-fit: cover;border-radius:30px;"
  240. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  241. <div class="d-flex flex-column">
  242. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.status.account.username}}</p>
  243. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  244. <span>{{report.status.account.followers_count}} Followers</span>
  245. <span>·</span>
  246. <span>Joined {{ timeAgo(report.status.account.created_at) }}</span>
  247. </div>
  248. </div>
  249. </div>
  250. </a>
  251. </td>
  252. <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
  253. <td class="align-middle"><a href="#" class="btn btn-primary btn-sm" @click.prevent="viewSpamReport(report)">View</a></td>
  254. </tr>
  255. </tbody>
  256. </table>
  257. </div>
  258. <div v-if="closedReportsFetched && closedReports && closedReports.data.length" class="d-flex align-items-center justify-content-center">
  259. <button
  260. class="btn btn-primary rounded-pill"
  261. :disabled="!closedReports.links.prev"
  262. @click="autospamPaginate('prev')">
  263. Prev
  264. </button>
  265. <button
  266. class="btn btn-primary rounded-pill"
  267. :disabled="!closedReports.links.next"
  268. @click="autospamPaginate('next')">
  269. Next
  270. </button>
  271. </div>
  272. </template>
  273. <template v-else>
  274. <div class="d-flex justify-content-center align-items-center py-5">
  275. <b-spinner />
  276. </div>
  277. </template>
  278. </div>
  279. <div v-else-if="this.tabIndex === 'manage_tokens'">
  280. <div class="row align-items-center mb-3">
  281. <div class="col-12 col-md-9">
  282. <div class="card card-body mb-0">
  283. <p class="mb-0">
  284. Tokens are used to split paragraphs and sentences into smaller units that can be more easily assigned meaning.
  285. </p>
  286. </div>
  287. </div>
  288. <div class="col-12 col-md-3">
  289. <a class="btn btn-primary btn-lg btn-block" href="#" @click.prevent="showCreateTokenModal = true">
  290. <i class="far fa-plus fa-lg mr-1"></i>
  291. Create New Token
  292. </a>
  293. </div>
  294. </div>
  295. <template v-if="customTokensFetched">
  296. <template v-if="customTokens && customTokens.data && customTokens.data.length">
  297. <div class="table-responsive rounded">
  298. <table class="table table-dark">
  299. <thead class="thead-dark">
  300. <tr>
  301. <th scope="col">ID</th>
  302. <th scope="col">Token</th>
  303. <th scope="col">Category</th>
  304. <th scope="col">Weight</th>
  305. <th scope="col">Created</th>
  306. <th scope="col">Edit</th>
  307. </tr>
  308. </thead>
  309. <tbody>
  310. <tr v-for="(token, idx) in customTokens.data" :key="'ct' + token.id + idx">
  311. <td class="font-weight-bold text-monospace text-muted align-middle">
  312. {{ token.id}}
  313. </td>
  314. <td class="align-middle">
  315. <p class="font-weight-bold mb-0">{{ token.token }}</p>
  316. </td>
  317. <td class="align-middle">
  318. <p class="text-capitalize mb-0">{{ token.category }}</p>
  319. </td>
  320. <td class="align-middle">
  321. <p class="text-capitalize mb-0">{{ token.weight }}</p>
  322. </td>
  323. <td class="font-weight-bold align-middle">{{ timeAgo(token.created_at) }}</td>
  324. <td class="font-weight-bold align-middle">
  325. <a class="btn btn-primary btn-sm font-weight-bold" href="#" @click.prevent="openEditTokenModal(token)">Edit</a>
  326. </td>
  327. </tr>
  328. </tbody>
  329. </table>
  330. </div>
  331. <div v-if="customTokensFetched && customTokens && customTokens.data.length" class="d-flex align-items-center justify-content-center">
  332. <button
  333. class="btn btn-primary rounded-pill"
  334. :disabled="!customTokens.prev_page_url"
  335. @click="autospamTokenPaginate('prev')">
  336. Prev
  337. </button>
  338. <button
  339. class="btn btn-primary rounded-pill"
  340. :disabled="!customTokens.next_page_url"
  341. @click="autospamTokenPaginate('next')">
  342. Next
  343. </button>
  344. </div>
  345. </template>
  346. <div v-else>
  347. <div class="card">
  348. <div class="card-body text-center py-5">
  349. <p class="pt-5">
  350. <i class="far fa-inbox fa-4x text-light"></i>
  351. </p>
  352. <p class="lead mb-5">No custom tokens found!</p>
  353. </div>
  354. </div>
  355. </div>
  356. </template>
  357. <template v-else>
  358. <div class="d-flex justify-content-center align-items-center py-5">
  359. <b-spinner />
  360. </div>
  361. </template>
  362. </div>
  363. <div v-else-if="this.tabIndex === 'import_export'">
  364. <div class="row">
  365. <div class="col-12">
  366. <div class="card card-body">
  367. <p class="mb-0">
  368. You can import and export Spam training data
  369. </p>
  370. <p class="mb-0 small">
  371. We recommend exercising caution when importing training data from untrusted parties!
  372. </p>
  373. </div>
  374. </div>
  375. </div>
  376. <div class="row">
  377. <div class="col-12 col-md-6">
  378. <div class="card bg-dark">
  379. <div class="card-header font-weight-bold">Import Training Data</div>
  380. <div class="card-body">
  381. <div class="d-flex flex-column align-items-center justify-content-center py-4" style="gap:1rem;">
  382. <p class="mb-0">
  383. <i class="far fa-plus-circle fa-5x text-light"></i>
  384. </p>
  385. <p class="lead text-lighter">Make sure the file you are importing is a valid training data export!</p>
  386. <button class="btn btn-primary btn-lg font-weight-bold btn-block" @click.prevent="handleImport">Upload Import</button>
  387. </div>
  388. </div>
  389. </div>
  390. </div>
  391. <div class="col-12 col-md-6">
  392. <div class="card bg-dark">
  393. <div class="card-header font-weight-bold">Export Training Data</div>
  394. <div class="card-body">
  395. <div class="d-flex flex-column align-items-center justify-content-center py-4" style="gap:1rem;">
  396. <p class="mb-0">
  397. <i class="far fa-download fa-5x text-light"></i>
  398. </p>
  399. <p class="lead text-lighter">Only share training data with people you trust. It can be used by spammers to bypass detection!</p>
  400. <button class="btn btn-primary btn-lg font-weight-bold btn-block" @click.prevent="downloadExport">Download Export</button>
  401. </div>
  402. </div>
  403. </div>
  404. </div>
  405. </div>
  406. </div>
  407. </div>
  408. </div>
  409. <b-modal v-model="showSpamReportModal" title="Autospam Post" :ok-only="true" ok-title="Close" ok-variant="outline-primary">
  410. <div v-if="viewingSpamReportLoading" class="d-flex align-items-center justify-content-center">
  411. <b-spinner />
  412. </div>
  413. <template v-else>
  414. <div class="list-group list-group-horizontal mt-3">
  415. <div v-if="viewingSpamReport && viewingSpamReport.status && viewingSpamReport.status.account" class="list-group-item d-flex align-items-center justify-content-between flex-column flex-grow-1" style="gap:0.4rem;">
  416. <div class="text-muted small font-weight-bold mt-n1">Reported Account</div>
  417. <a v-if="viewingSpamReport.status.account && viewingSpamReport.status.account.id" :href="`/i/web/profile/${viewingSpamReport.status.account.id}`" target="_blank" class="text-primary">
  418. <div class="d-flex align-items-center" style="gap:0.61rem;">
  419. <img
  420. :src="viewingSpamReport.status.account.avatar"
  421. width="30"
  422. height="30"
  423. style="object-fit: cover;border-radius:30px;"
  424. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  425. <div class="d-flex flex-column">
  426. <p class="font-weight-bold mb-0 text-break" style="font-size: 12px;max-width: 140px;line-height: 16px;" :class="[ viewingSpamReport.status.account.is_admin ? 'text-danger': '']">@{{viewingSpamReport.status.account.acct}}</p>
  427. <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
  428. <span>{{viewingSpamReport.status.account.followers_count}} Followers</span>
  429. <span>·</span>
  430. <span>Joined {{ timeAgo(viewingSpamReport.status.account.created_at) }}</span>
  431. </div>
  432. </div>
  433. </div>
  434. </a>
  435. </div>
  436. </div>
  437. <div v-if="viewingSpamReport && viewingSpamReport.status" class="list-group mt-3">
  438. <div v-if="viewingSpamReport && viewingSpamReport.status && viewingSpamReport.status.media_attachments.length" class="list-group-item d-flex flex-column flex-grow-1" style="gap:0.4rem;">
  439. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  440. <div>Reported Post</div>
  441. <a class="font-weight-bold" :href="viewingSpamReport.status.url" target="_blank">View</a>
  442. </div>
  443. <img
  444. v-if="viewingSpamReport.status.media_attachments[0].type === 'image'"
  445. :src="viewingSpamReport.status.media_attachments[0].url"
  446. height="140"
  447. class="rounded"
  448. style="object-fit: cover;"
  449. onerror="this.src='/storage/no-preview.png';this.error=null;" />
  450. <video
  451. v-else-if="viewingSpamReport.status.media_attachments[0].type === 'video'"
  452. height="140"
  453. controls
  454. :src="viewingSpamReport.status.media_attachments[0].url"
  455. onerror="this.src='/storage/no-preview.png';this.onerror=null;"
  456. ></video>
  457. </div>
  458. <div
  459. v-if="viewingSpamReport &&
  460. viewingSpamReport.status &&
  461. viewingSpamReport.status.content_text &&
  462. viewingSpamReport.status.content_text.length"
  463. class="list-group-item d-flex flex-column flex-grow-1"
  464. style="gap:0.4rem;">
  465. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  466. <div>Reported Post Caption</div>
  467. <a class="font-weight-bold" :href="viewingSpamReport.status.url" target="_blank">View</a>
  468. </div>
  469. <p class="mb-0 read-more" style="font-size:12px;overflow-y: hidden">{{ viewingSpamReport.status.content_text }}</p>
  470. </div>
  471. </div>
  472. </template>
  473. </b-modal>
  474. <b-modal v-model="showNonSpamModal" title="Train Non-Spam" :ok-only="true" ok-title="Close" ok-variant="outline-primary">
  475. <p class="small font-weight-bold">Select trusted accounts to train non-spam posts against!</p>
  476. <autocomplete
  477. v-if="!nonSpamAccounts || nonSpamAccounts.length < 10"
  478. :search="composeSearch"
  479. :disabled="searchLoading"
  480. placeholder="Search by username"
  481. aria-label="Search by username"
  482. :get-result-value="getTagResultValue"
  483. @submit="onSearchResultClick"
  484. ref="autocomplete"
  485. >
  486. <template #result="{ result, props }">
  487. <li
  488. v-bind="props"
  489. class="autocomplete-result d-flex align-items-center"
  490. style="gap: 0.5rem"
  491. >
  492. <img :src="result.avatar" width="32" height="32" class="rounded-circle" onerror="this.src='/storage/avatars/default.png';this.error=null;">
  493. <div class="font-weight-bold">
  494. {{ result.username }}
  495. </div>
  496. </li>
  497. </template>
  498. </autocomplete>
  499. <div class="list-group mt-3">
  500. <div
  501. v-for="(acct, idx) in nonSpamAccounts"
  502. class="list-group-item">
  503. <div class="d-flex align-items-center justify-content-between">
  504. <div class="d-flex flex-row align-items-center" style="gap: 0.5rem">
  505. <img :src="acct.avatar" width="32" height="32" class="rounded-circle" onerror="this.src='/storage/avatars/default.png';this.error=null;">
  506. <div class="font-weight-bold">
  507. {{ acct.username }}
  508. </div>
  509. </div>
  510. <a class="text-danger" href="#" @click.prevent="autospamTrainNonSpamRemove(idx)">
  511. <i class="fas fa-trash"></i>
  512. </a>
  513. </div>
  514. </div>
  515. </div>
  516. <div
  517. v-if="nonSpamAccounts && nonSpamAccounts.length"
  518. class="mt-3">
  519. <a class="btn btn-primary btn-lg font-weight-bold btn-block" href="#" @click.prevent="autospamTrainNonSpamSubmit">Train non-spam posts on trusted accounts</a>
  520. </div>
  521. </b-modal>
  522. <b-modal
  523. v-model="showCreateTokenModal"
  524. title="Create New Token"
  525. cancel-title="Close"
  526. cancel-variant="outline-primary"
  527. ok-title="Save"
  528. ok-variant="primary"
  529. v-on:ok="handleSaveToken">
  530. <div class="list-group mt-3">
  531. <div class="list-group-item">
  532. <div class="row align-items-center">
  533. <div class="col-4">
  534. <p class="mb-0 font-weight-bold small">Token</p>
  535. </div>
  536. <div class="col-8">
  537. <input class="form-control" v-model="customTokenForm.token" />
  538. </div>
  539. </div>
  540. </div>
  541. <div class="list-group-item">
  542. <div class="row align-items-center">
  543. <div class="col-4">
  544. <p class="mb-0 font-weight-bold small">Weight</p>
  545. </div>
  546. <div class="col-8">
  547. <input type="number" class="form-control" min="-128" max="128" step="1" v-model="customTokenForm.weight" />
  548. </div>
  549. </div>
  550. </div>
  551. <div class="list-group-item">
  552. <div class="row align-items-center">
  553. <div class="col-4">
  554. <p class="mb-0 font-weight-bold small">Category</p>
  555. </div>
  556. <div class="col-8">
  557. <select class="form-control" v-model="customTokenForm.category">
  558. <option value="spam">Is Spam</option>
  559. <option value="ham">Is NOT Spam</option>
  560. </select>
  561. </div>
  562. </div>
  563. </div>
  564. <div class="list-group-item">
  565. <div class="row align-items-center">
  566. <div class="col-4">
  567. <p class="mb-0 font-weight-bold small">Note</p>
  568. </div>
  569. <div class="col-8">
  570. <textarea class="form-control" v-model="customTokenForm.note"></textarea>
  571. </div>
  572. </div>
  573. </div>
  574. <div class="list-group-item">
  575. <div class="row align-items-center">
  576. <div class="col-4">
  577. <p class="mb-0 font-weight-bold small">Active</p>
  578. </div>
  579. <div class="col-8 text-right">
  580. <div class="custom-control custom-checkbox">
  581. <input type="checkbox" class="custom-control-input" id="customCheck1" v-model="customTokenForm.active">
  582. <label class="custom-control-label" for="customCheck1"></label>
  583. </div>
  584. </div>
  585. </div>
  586. </div>
  587. </div>
  588. </b-modal>
  589. <b-modal
  590. v-model="showEditTokenModal"
  591. title="Edit Token"
  592. cancel-title="Close"
  593. cancel-variant="outline-primary"
  594. ok-title="Update"
  595. ok-variant="primary"
  596. v-on:ok="handleUpdateToken">
  597. <div class="list-group mt-3">
  598. <div class="list-group-item">
  599. <div class="row align-items-center">
  600. <div class="col-4">
  601. <p class="mb-0 font-weight-bold small">Token</p>
  602. </div>
  603. <div class="col-8">
  604. <input class="form-control" :value="editCustomTokenForm.token" disabled/>
  605. </div>
  606. </div>
  607. </div>
  608. <div class="list-group-item">
  609. <div class="row align-items-center">
  610. <div class="col-4">
  611. <p class="mb-0 font-weight-bold small">Weight</p>
  612. </div>
  613. <div class="col-8">
  614. <input type="number" class="form-control" min="-128" max="128" step="1" v-model="editCustomTokenForm.weight" />
  615. </div>
  616. </div>
  617. </div>
  618. <div class="list-group-item">
  619. <div class="row align-items-center">
  620. <div class="col-4">
  621. <p class="mb-0 font-weight-bold small">Category</p>
  622. </div>
  623. <div class="col-8">
  624. <select class="form-control" v-model="editCustomTokenForm.category">
  625. <option value="spam">Is Spam</option>
  626. <option value="ham">Is NOT Spam</option>
  627. </select>
  628. </div>
  629. </div>
  630. </div>
  631. <div class="list-group-item">
  632. <div class="row align-items-center">
  633. <div class="col-4">
  634. <p class="mb-0 font-weight-bold small">Note</p>
  635. </div>
  636. <div class="col-8">
  637. <textarea class="form-control" v-model="editCustomTokenForm.note"></textarea>
  638. </div>
  639. </div>
  640. </div>
  641. <div class="list-group-item">
  642. <div class="row align-items-center">
  643. <div class="col-4">
  644. <p class="mb-0 font-weight-bold small">Active</p>
  645. </div>
  646. <div class="col-8 text-right">
  647. <div class="custom-control custom-checkbox">
  648. <input type="checkbox" class="custom-control-input" id="customCheck1" v-model="editCustomTokenForm.active">
  649. <label class="custom-control-label" for="customCheck1"></label>
  650. </div>
  651. </div>
  652. </div>
  653. </div>
  654. </div>
  655. </b-modal>
  656. </div>
  657. </template>
  658. <script type="text/javascript">
  659. import Autocomplete from '@trevoreyre/autocomplete-vue'
  660. import '@trevoreyre/autocomplete-vue/dist/style.css'
  661. export default {
  662. components: {
  663. Autocomplete,
  664. },
  665. data() {
  666. return {
  667. loaded: false,
  668. tabIndex: 0,
  669. config: {
  670. autospam_enabled: null,
  671. open: 0,
  672. closed: 0
  673. },
  674. closedReports: [],
  675. closedReportsFetched: false,
  676. closedReportsCursor: null,
  677. closedReportsCanLoadMore: false,
  678. showSpamReportModal: false,
  679. showSpamReportModalLoading: true,
  680. viewingSpamReport: undefined,
  681. viewingSpamReportLoading: false,
  682. showNonSpamModal: false,
  683. nonSpamAccounts: [],
  684. searchLoading: false,
  685. customTokens: [],
  686. customTokensFetched: false,
  687. customTokensCanLoadMore: false,
  688. showCreateTokenModal: false,
  689. customTokenForm: {
  690. token: undefined,
  691. weight: 1,
  692. category: 'spam',
  693. note: undefined,
  694. active: true
  695. },
  696. showEditTokenModal: false,
  697. editCustomToken: {},
  698. editCustomTokenForm: {
  699. token: undefined,
  700. weight: 1,
  701. category: 'spam',
  702. note: undefined,
  703. active: true
  704. }
  705. }
  706. },
  707. mounted() {
  708. setTimeout(() => {
  709. this.loaded = true;
  710. this.fetchConfig();
  711. }, 1000);
  712. },
  713. methods: {
  714. toggleTab(idx) {
  715. this.tabIndex = idx;
  716. if(idx == 0) {
  717. setTimeout(() => {
  718. this.initChart();
  719. }, 500);
  720. }
  721. if(idx === 'closed_reports' && !this.closedReportsFetched) {
  722. this.fetchClosedReports();
  723. }
  724. if(idx === 'manage_tokens' && !this.customTokensFetched) {
  725. this.fetchCustomTokens();
  726. }
  727. },
  728. formatCount(ct) {
  729. return App.util.format.count(ct);
  730. },
  731. timeAgo(str) {
  732. if(!str) {
  733. return str;
  734. }
  735. return App.util.format.timeAgo(str);
  736. },
  737. fetchConfig() {
  738. axios.post('/i/admin/api/autospam/config')
  739. .then(res => {
  740. this.config = res.data;
  741. this.loaded = true;
  742. })
  743. .finally(() => {
  744. setTimeout(() => {
  745. this.initChart();
  746. }, 100);
  747. })
  748. },
  749. initChart() {
  750. var usersChart = new Chart(document.querySelector('#c1-dark'), {
  751. type: 'line',
  752. options: {
  753. scales: {
  754. yAxes: [{
  755. gridLines: {
  756. lineWidth: 1,
  757. color: '#212529',
  758. zeroLineColor: '#212529'
  759. },
  760. }]
  761. },
  762. },
  763. data: {
  764. datasets: [{
  765. data: this.config.graph
  766. }],
  767. labels: this.config.graphLabels
  768. }
  769. });
  770. },
  771. fetchClosedReports(url = '/i/admin/api/autospam/reports/closed') {
  772. axios.post(url)
  773. .then(res => {
  774. this.closedReports = res.data;
  775. })
  776. .finally(() => {
  777. this.closedReportsFetched = true;
  778. })
  779. },
  780. viewSpamReport(report) {
  781. this.viewingSpamReportLoading = false;
  782. this.viewingSpamReport = report;
  783. this.showSpamReportModal = true;
  784. setTimeout(() => {
  785. pixelfed.readmore()
  786. }, 500)
  787. },
  788. autospamPaginate(dir) {
  789. event.currentTarget.blur();
  790. let url = dir == 'next' ? this.closedReports.links.next : this.closedReports.links.prev;
  791. this.fetchClosedReports(url);
  792. },
  793. autospamTrainSpam() {
  794. event.currentTarget.blur();
  795. axios.post('/i/admin/api/autospam/train')
  796. .then(res => {
  797. swal('Training Autospam!', 'A background job has been dispatched to train Autospam!', 'success');
  798. setTimeout(() => {
  799. window.location.reload();
  800. }, 10000);
  801. })
  802. .catch(error => {
  803. if(error.response.status === 422) {
  804. swal('Error', error.response.data.error, 'error');
  805. } else {
  806. swal('Error', 'Oops, an error occured, please try again later', 'error');
  807. }
  808. })
  809. },
  810. autospamTrainNonSpam() {
  811. this.showNonSpamModal = true;
  812. },
  813. composeSearch(input) {
  814. if (input.length < 1) { return []; };
  815. return axios.post('/i/admin/api/autospam/search/non-spam', {
  816. q: input,
  817. }).then(res => {
  818. let data = res.data.filter(a => {
  819. if(!this.nonSpamAccounts || !this.nonSpamAccounts.length) {
  820. return true;
  821. }
  822. return this.nonSpamAccounts && this.nonSpamAccounts.map(a => a.id).indexOf(a.id) == -1;
  823. })
  824. return data;
  825. });
  826. },
  827. getTagResultValue(result) {
  828. return result.username;
  829. },
  830. onSearchResultClick(result) {
  831. if(this.nonSpamAccounts.map(a => a.id).indexOf(result.id) != -1) {
  832. return;
  833. }
  834. this.nonSpamAccounts.push(result);
  835. return;
  836. },
  837. autospamTrainNonSpamRemove(idx) {
  838. this.nonSpamAccounts.splice(idx, 1);
  839. },
  840. autospamTrainNonSpamSubmit() {
  841. this.showNonSpamModal = false;
  842. axios.post('/i/admin/api/autospam/train/non-spam', {
  843. accounts: this.nonSpamAccounts
  844. })
  845. .then(res => {
  846. swal('Training Autospam!', 'A background job has been dispatched to train Autospam!', 'success');
  847. setTimeout(() => {
  848. window.location.reload();
  849. }, 10000);
  850. })
  851. .catch(error => {
  852. if(error.response.status === 422) {
  853. swal('Error', error.response.data.error, 'error');
  854. } else {
  855. swal('Error', 'Oops, an error occured, please try again later', 'error');
  856. }
  857. })
  858. },
  859. fetchCustomTokens(url = '/i/admin/api/autospam/tokens/custom') {
  860. axios.post(url)
  861. .then(res => {
  862. this.customTokens = res.data;
  863. })
  864. .finally(() => {
  865. this.customTokensFetched = true;
  866. })
  867. },
  868. handleSaveToken() {
  869. axios.post('/i/admin/api/autospam/tokens/store', this.customTokenForm)
  870. .then(res => {
  871. console.log(res.data);
  872. })
  873. .catch(err => {
  874. swal('Oops! An Error Occured', err.response.data.message, 'error');
  875. })
  876. .finally(() => {
  877. this.customTokenForm = {
  878. token: undefined,
  879. weight: 1,
  880. category: 'spam',
  881. note: undefined,
  882. active: true
  883. }
  884. this.fetchCustomTokens();
  885. })
  886. },
  887. openEditTokenModal(token) {
  888. event.currentTarget.blur();
  889. this.editCustomToken = token;
  890. this.editCustomTokenForm = token;
  891. this.showEditTokenModal = true;
  892. },
  893. handleUpdateToken() {
  894. axios.post('/i/admin/api/autospam/tokens/update', this.editCustomTokenForm)
  895. .then(res => {
  896. console.log(res.data);
  897. })
  898. },
  899. autospamTokenPaginate(dir) {
  900. event.currentTarget.blur();
  901. let url = dir == 'next' ? this.customTokens.next_page_url : this.customTokens.prev_page_url;
  902. this.fetchCustomTokens(url);
  903. },
  904. downloadExport() {
  905. event.currentTarget.blur();
  906. axios.post('/i/admin/api/autospam/tokens/export', {}, {
  907. responseType: 'blob'
  908. })
  909. .then(res => {
  910. const aElement = document.createElement('a');
  911. aElement.setAttribute('download', 'pixelfed-autospam-export.json');
  912. const href = URL.createObjectURL(res.data);
  913. aElement.href = href;
  914. aElement.setAttribute('target', '_blank');
  915. aElement.click();
  916. URL.revokeObjectURL(href);
  917. })
  918. .catch(async(error) => {
  919. let errorString = error.response.data
  920. if (
  921. error.request.responseType === 'blob' &&
  922. error.response.data instanceof Blob &&
  923. error.response.data.type &&
  924. error.response.data.type.toLowerCase().indexOf('json') != -1
  925. ) {
  926. errorString = JSON.parse(await error.response.data.text());
  927. swal('Export Error', errorString.error, 'error');
  928. };
  929. });
  930. },
  931. enableAdvanced() {
  932. event.currentTarget.blur();
  933. if(
  934. !this.config.files.spam.exists ||
  935. !this.config.files.ham.exists ||
  936. !this.config.files.combined.exists ||
  937. this.config.files.spam.size < 1000 ||
  938. this.config.files.ham.size < 1000 ||
  939. this.config.files.combined.size < 1000
  940. ) {
  941. swal('Training Required', 'Before you can enable Advanced Detection, you need to train the models.\n\n Click on the "Train Autospam" tab and train both categories before proceeding', 'error');
  942. return;
  943. }
  944. swal({
  945. title: "Confirm",
  946. text: "Are you sure you want to enable Advanced Detection?",
  947. icon: "warning",
  948. dangerMode: true,
  949. buttons: {
  950. cancel: "Cancel",
  951. confirm: {
  952. text: "Enable",
  953. value: "enable",
  954. }
  955. },
  956. })
  957. .then((res) => {
  958. if (res === 'enable') {
  959. axios.post('/i/admin/api/autospam/config/enable')
  960. .then(res => {
  961. swal("Success! Advanced Detection is now enabled!\n\n This page will reload in a few seconds!", {
  962. icon: "success",
  963. });
  964. setTimeout(() => {
  965. window.location.reload();
  966. }, 5000);
  967. })
  968. .catch(err => {
  969. swal('Oops!', 'An error occured, please try again later', 'error');
  970. })
  971. } else {
  972. }
  973. });
  974. },
  975. disableAdvanced() {
  976. event.currentTarget.blur();
  977. swal({
  978. title: "Confirm",
  979. text: "Are you sure you want to disable Advanced Detection?",
  980. icon: "warning",
  981. dangerMode: true,
  982. buttons: {
  983. cancel: "Cancel",
  984. confirm: {
  985. text: "Disable",
  986. value: "disable",
  987. }
  988. },
  989. })
  990. .then((res) => {
  991. if (res === 'disable') {
  992. axios.post('/i/admin/api/autospam/config/disable')
  993. .then(res => {
  994. swal("Success! Advanced Detection is now disabled!\n\n This page will reload in a few seconds!", {
  995. icon: "success",
  996. });
  997. setTimeout(() => {
  998. window.location.reload();
  999. }, 5000);
  1000. })
  1001. .catch(err => {
  1002. swal('Oops!', 'An error occured, please try again later', 'error');
  1003. })
  1004. }
  1005. })
  1006. },
  1007. handleImport() {
  1008. event.currentTarget.blur();
  1009. swal('Error', 'You do not have enough data to support importing.', 'error');
  1010. }
  1011. }
  1012. }
  1013. </script>