├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── CHANGELOG.md ├── GiveawayCompanion.user.js ├── LICENSE ├── README.md └── images └── script_bar.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://yoomoney.ru/to/410011391610131'] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: bug 5 | --- 6 | 7 | 8 | 9 | 10 | - Browser: 11 | - Extension: Tampermonkey/Violentmonkey -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Create a report to request a new feature 4 | labels: enhancement 5 | --- -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | ### v1.7.8 (2025-05-18) 3 | * Steam: fixed leaving the group. 4 | * Steam: added link to steamstat.us in error messages. 5 | ### v1.7.7 (2025-02-13) 6 | * Steam: fixed loading of tasks related to adult games. 7 | ### v1.7.6 (2024-11-25) 8 | * Steam: fixed loading of tasks related to games. 9 | * Removed support for key.gift. 10 | * Removed support for chubkeys.com. 11 | ### v1.7.5 (2024-06-03) 12 | * KeyJoker: added task confirmation. 13 | * KeyJoker: fixed infinite loading of Steam tasks (it happened because of captcha, now the script doesn't retry to load failed tasks). 14 | ### v1.7.4 (2024-04-20) 15 | * Givee.club: added support for "follow a game" tasks. 16 | ### v1.7.3 (2024-01-04) 17 | * bananagiveaway.com has been replaced by bananatic.com. 18 | ### v1.7.2 (2023-12-18) 19 | * Gleam: added completion of Twitter and TikTok tasks. 20 | * Gleam: added completion of tasks that require a correct answer from the user (disabled by default, can be enabled at the top of the script in answerQuestionsWithCheck block). 21 | * Gleam: added filling in username in Twitter tasks (disabled by default, can be enabled at the top of the script in twitterSetUsername block). 22 | * Gleam: added filling in username in TikTok tasks (disabled by default, can be enabled at the top of the script in tiktokSetUsername block). 23 | * Giveaway.su: fixed Steam groups. 24 | ### v1.7.1 (2023-09-04) 25 | * New type of tasks: add a game to Steam library. 26 | * Opquests: added support for "add a game to Steam library" tasks. 27 | * Gleam: added another type of simple tasks to complete. 28 | * Gleam: added completion of tasks that require an answer from the user (disabled by default, can be enabled at the top of the script in answerQuestions block). 29 | ### v1.7 (2023-06-12) 30 | * Added support for key.gift. 31 | * Gleam: added simple task completion and task confirmation. 32 | ### v1.6 (2022-12-30) 33 | * New type of tasks: add a game to wishlist on Steam. 34 | * New type of tasks: follow a game on Steam. 35 | * Added support for givee.club. 36 | * Added support for opquests.com. 37 | * Key-hub.eu: added support for "add a game to wishlist on Steam" tasks. 38 | * Removed support for marvelousga.com. 39 | * Removed support for indiegala.com. 40 | * Removed support for givekey.ru. 41 | * Removed support for takekey.ru. 42 | ### v1.5.1 (2021-01-29) 43 | * Gamehag: fixed completion of "Play one of our free games" tasks. 44 | ### v1.5 (2020-06-16) 45 | * Added support for key-hub.eu (tasks, groups). 46 | * Added support for takekey.ru (groups, keys). 47 | * Removed support for gamecode.win (dead). 48 | * Removed support for dupedornot.com (dead). 49 | * Removed support for orlygift.com ("We’ll take a break" for too long). 50 | ### v1.4.3 (2020-01-15) 51 | * Gamehunt: replaced by Givekey.ru (the same site on a different domain). 52 | * Giveawayhopper: support removed (the site is dead). 53 | ### v1.4.2 (2019-09-17) 54 | * Chubkeys: updated for new design and URL. 55 | * Gamezito: support removed. 56 | * Groups: notifications updated. 57 | ### v1.4.1 (2019-06-14) 58 | * Groups: added a notification of trying to join a private group. 59 | ### v1.4 (2019-05-13) 60 | * Added support for keyjoker.com (groups, keys). 61 | * GiveawayHopper: updated for new design. 62 | * Gamehag: added auto completion of survey tasks. 63 | * Added notification about successful script update. 64 | ### v1.3.1 (2019-01-26) 65 | * Giveaway.su: removed support for tasks completion ([the reason](https://github.com/longnull/GiveawayCompanion/issues/1#issuecomment-457699811)). 66 | * Fixed a small bug with scroll visibility. 67 | *** 68 | ### v1.3 (2019-01-22) 69 | * Giveaway.su: added support for tasks completion without browser extension, the script can completely cheat the verification of tasks (for more information click "i" button on the script bar). 70 | * Giveaway.su: added support for Steam key activation. 71 | * Giveaway.su: Steam groups shortened by bit.ly are now also processed. 72 | * Groups: now loading is performed simultaneously, not one by one. 73 | * Notifications: added close button. 74 | * Fixed positioning of the script bar, now it will not go beyond the bounds of the document in gleam.io iframes. 75 | *** 76 | ### v1.2 (2018-12-01) 77 | * Added support for giveaway.su (groups). 78 | *** 79 | ### v1.1.1 (2018-11-27) 80 | * Gamehag: the script now works with all languages. 81 | * Gamehag: the script no longer trying to complete tasks that don't have a "verify" button. 82 | *** 83 | ### v1.1 (2018-11-13) 84 | * Added support for giveawayhopper.com (tasks, groups, keys). 85 | * Added support for chubkeys.com (groups, keys). 86 | *** 87 | ### v1.0 (2018-11-05) 88 | * Initial release. 89 | 90 | *** 91 | 92 | ## Список изменений 93 | ### v1.7.8 (2025-05-18) 94 | * Steam: исправлен выход из группы. 95 | * Steam: в сообщения об ошибках добавлена ссылка на steamstat.us. 96 | ### v1.7.7 (2025-02-13) 97 | * Steam: исправлена загрузка заданий связанных с играми для взрослых. 98 | ### v1.7.6 (2024-11-25) 99 | * Steam: исправлена загрузка заданий связанных с играми. 100 | * Удалена поддержка key.gift. 101 | * Удалена поддержка chubkeys.com. 102 | ### v1.7.5 (2024-06-03) 103 | * KeyJoker: добавлено подтверждение заданий. 104 | * KeyJoker: исправлена бесконечная загрузка заданий Steam (это происходило из-за капчи, теперь скрипт не пытается повторно загрузить неудачные задания). 105 | ### v1.7.4 (2024-04-20) 106 | * Givee.club: добавлена поддержка заданий "подписаться на игру". 107 | ### v1.7.3 (2024-01-04) 108 | * bananagiveaway.com заменён на bananatic.com. 109 | ### v1.7.2 (2023-12-18) 110 | * Gleam: добавлено выполнение Twitter и TikTok заданий. 111 | * Gleam: добавлено выполнение заданий, требующих правильного ответа от пользователя (выключено по умолчанию, включается вверху скрипта в блоке answerQuestionsWithCheck). 112 | * Gleam: добавлено заполнение имени пользователя в Twitter заданиях (выключено по умолчанию, включается вверху скрипта в блоке twitterSetUsername). 113 | * Gleam: добавлено заполнение имени пользователя в TikTok заданиях (выключено по умолчанию, включается вверху скрипта в блоке tiktokSetUsername). 114 | * Giveaway.su: исправлены группы Steam. 115 | ### v1.7.1 (2023-09-04) 116 | * Новый тип заданий: добавить игру в библиотеку Steam. 117 | * Opquests: добавлена поддержка заданий "добавить игру в библиотеку Steam". 118 | * Gleam: добавлен ещё один тип простых заданий для выполнения. 119 | * Gleam: добавлено выполнение заданий, требующих ввода от пользователя (выключено по умолчанию, включается вверху скрипта в блоке answerQuestions). 120 | ### v1.7 (2023-06-12) 121 | * Добавлена поддержка key.gift. 122 | * Gleam: добавлено выполнение простых заданий и подтверждение заданий. 123 | ### v1.6 (2022-12-30) 124 | * Новый тип заданий: добавить игру в список желаемого Steam. 125 | * Новый тип заданий: подписаться на игру в Steam. 126 | * Добавлена поддержка givee.club. 127 | * Добавлена поддержка opquests.com. 128 | * Key-hub.eu: добавлена поддержка заданий "добавить игру в список желаемого Steam". 129 | * Удалена поддержка marvelousga.com. 130 | * Удалена поддержка indiegala.com. 131 | * Удалена поддержка givekey.ru. 132 | * Удалена поддержка takekey.ru. 133 | ### v1.5.1 (2021-01-29) 134 | * Gamehag: исправлено выполнение заданий "Play one of our free games". 135 | ### v1.5 (2020-06-16) 136 | * Добавлена поддержка key-hub.eu (задания, группы). 137 | * Добавлена поддержка takekey.ru (группы, ключи). 138 | * Удалена поддержка gamecode.win (мёртв). 139 | * Удалена поддержка dupedornot.com (мёртв). 140 | * Удалена поддержка orlygift.com ("We’ll take a break" слишком долго). 141 | ### v1.4.3 (2020-01-15) 142 | * Gamehunt: заменён на Givekey.ru (тот же сайт на другом домене). 143 | * Giveawayhopper: поддержка удалена (сайт мёртв). 144 | ### v1.4.2 (2019-09-17) 145 | * Chubkeys: обновлён под новый дизайн и URL. 146 | * Gamezito: поддержка удалена. 147 | * Группы: обновлены уведомления. 148 | ### v1.4.1 (2019-06-14) 149 | * Группы: добавлено уведомление о попытке вступления в приватную группу. 150 | ### v1.4 (2019-05-13) 151 | * Добавлена поддержка keyjoker.com (группы, ключи). 152 | * GiveawayHopper: обновлён под новый дизайн. 153 | * Gamehag: добавлено автовыполнение анкетных заданий. 154 | * Добавлено уведомление об успешном обновлении скрипта. 155 | ### v1.3.1 (2019-01-26) 156 | * Giveaway.su: удалена поддержка выполнения заданий ([причина](https://github.com/longnull/GiveawayCompanion/issues/1#issuecomment-457699811)). 157 | * Исправлена ​​небольшая ошибка с видимостью прокрутки. 158 | *** 159 | ### v1.3 (2019-01-22) 160 | * Giveaway.su: добавлена поддержка выполнения заданий без браузерного расширения, скрипт может полностью обманывать проверку заданий (для дополнительной информации кликните кнопку "i" на панели скрипта). 161 | * Giveaway.su: добавлена поддержка активации Steam ключей. 162 | * Giveaway.su: Steam группы, укороченные с помощью bit.ly, теперь тоже обрабатываются. 163 | * Группы: теперь загрузка происходит одновременно, а не одна за одной. 164 | * Уведомления: добавлена кнопка закрытия. 165 | * Исправлено позиционирование панели скрипта, теперь панель не будет выходить за рамки документа во фреймах gleam.io. 166 | *** 167 | ### v1.2 (2018-12-01) 168 | * Добавлена поддержка giveaway.su (группы). 169 | *** 170 | ### v1.1.1 (2018-11-27) 171 | * Gamehag: теперь скрипт работает со всеми языками. 172 | * Gamehag: теперь скрипт не пытается выполнять задания без кнопки "проверить". 173 | *** 174 | ### v1.1 (2018-11-13) 175 | * Добавлена поддержка giveawayhopper.com (задания, группы, ключи). 176 | * Добавлена поддержка chubkeys.com (группы, ключи). 177 | *** 178 | ### v1.0 (2018-11-05) 179 | * Первая версия. -------------------------------------------------------------------------------- /GiveawayCompanion.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Giveaway Companion 3 | // @description Saves your time on games giveaway sites 4 | // @description:ru Экономит ваше время на сайтах с раздачами игр 5 | // @author longnull 6 | // @namespace longnull 7 | // @version 1.7.8 8 | // @homepage https://github.com/longnull/GiveawayCompanion 9 | // @supportURL https://github.com/longnull/GiveawayCompanion/issues 10 | // @updateURL https://raw.githubusercontent.com/longnull/GiveawayCompanion/master/GiveawayCompanion.user.js 11 | // @downloadURL https://raw.githubusercontent.com/longnull/GiveawayCompanion/master/GiveawayCompanion.user.js 12 | // @match *://*.grabfreegame.com/giveaway/* 13 | // @match *://*.bananatic.com/*/giveaway/* 14 | // @match *://*.gamingimpact.com/giveaway/* 15 | // @match *://*.whosgamingnow.net/giveaway/* 16 | // @match *://*.gamehag.com/* 17 | // @match *://*.gleam.io/*/* 18 | // @match *://*.giveaway.su/giveaway/view/* 19 | // @match *://*.keyjoker.com/* 20 | // @match *://*.key-hub.eu/giveaway/* 21 | // @match *://*.givee.club/*/event/* 22 | // @match *://*.opquests.com/* 23 | // @connect steamcommunity.com 24 | // @connect grabfreegame.com 25 | // @connect bananatic.com 26 | // @connect gamingimpact.com 27 | // @connect * 28 | // @grant GM_setValue 29 | // @grant GM.setValue 30 | // @grant GM_getValue 31 | // @grant GM.getValue 32 | // @grant GM_xmlhttpRequest 33 | // @grant GM.xmlHttpRequest 34 | // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js 35 | // ==/UserScript== 36 | 37 | (async () => { 38 | 'use strict'; 39 | 40 | const version = { 41 | string: '1.7.8', 42 | changes: { 43 | default: 44 | ``, 48 | ru: 49 | `` 53 | } 54 | }; 55 | 56 | const config = { 57 | // Output debug info into console (false - disabled, true - enabled) 58 | // Выводить отладочную информацию в консоль (false - выключено, true - включено) 59 | debug: false, 60 | // Features related to Steam groups (false - disabled, true - enabled) 61 | // Функционал связанный с группами Steam (false - выключено, true - включено) 62 | steamGroups: true, 63 | // Features related to Steam wishlist (false - disabled, true - enabled) 64 | // Функционал связанный со списком желаемого Steam (false - выключено, true - включено) 65 | steamAppWishlist: true, 66 | // Features related to application following in Steam (false - disabled, true - enabled) 67 | // Функционал связанный с подпиской на приложения в Steam (false - выключено, true - включено) 68 | steamAppFollow: true, 69 | // Features related to adding an application to Steam library (false - disabled, true - enabled) 70 | // Функционал связанный с добавлением приложения в библиотеку Steam (false - выключено, true - включено) 71 | steamAppAddToLibrary: true, 72 | // Size of the buttons (pixels) 73 | // Размер кнопок (пиксели) 74 | buttonsSize: 40, 75 | notifications: { 76 | // Maximum number of notifications 77 | // Максимальное количество уведомлений 78 | maxCount: 5, 79 | // Newest notifications on top (false - disabled, true - enabled) 80 | // Новые уведомления вверху (false - выключено, true - включено) 81 | newestOnTop: false, 82 | // Notifications width (pixels) 83 | // Ширина уведомлений (пиксели) 84 | width: 400, 85 | // How long the notification will display without user interaction (milliseconds, 0 - infinite, can be overridden) 86 | // Как долго будет отображаться уведомление без пользовательского взаимодействия (миллисекунды, 0 - бесконечно, может быть переопределено) 87 | timeout: 10000, 88 | // How long the notification will display after the user moves the mouse out of timed out notification (milliseconds) 89 | // Как долго будет отображаться "просроченное" уведомление после того, как пользователь убрал курсор (миллисекунды) 90 | extendedTimeout: 3000, 91 | // Close notification on click (can be overridden) 92 | // Закрывать уведомление по клику (может быть переопределено) 93 | closeOnClick: false 94 | }, 95 | // Sites settings 96 | // Настройки сайтов 97 | sitesConfig: { 98 | // Settings for Gleam 99 | // Настройки для Gleam 100 | gleam: { 101 | // Completion of tasks that require input from the user 102 | // Выполнение заданий, требующих ввода от пользователя 103 | answerQuestions: { 104 | // false - disabled, true - enabled 105 | // false - выключено, true - включено 106 | enabled: false, 107 | // Text 108 | // Текст 109 | answer: 'Yes' 110 | }, 111 | // Completion of tasks that require a correct answer from the user (client-side verification) 112 | // Выполнение заданий, требующих правильного ответа от пользователя (проверка на стороне клиента) 113 | answerQuestionsWithCheck: { 114 | // false - disabled, true - enabled 115 | // false - выключено, true - включено 116 | enabled: false 117 | }, 118 | // Fill in username in Twitter tasks 119 | // Заполнить имя пользователя в Twitter заданиях 120 | twitterSetUsername: { 121 | // false - disabled, true - enabled 122 | // false - выключено, true - включено 123 | enabled: false, 124 | // Username (if empty, the script will take the username from the email) 125 | // Имя (если пусто, то скрипт возьмёт имя из email) 126 | username: '' 127 | }, 128 | // Fill in username in TikTok tasks 129 | // Заполнить имя пользователя в TikTok заданиях 130 | tiktokSetUsername: { 131 | // false - disabled, true - enabled 132 | // false - выключено, true - включено 133 | enabled: false, 134 | // Username (if empty, the script will take the username from the email) 135 | // Имя (если пусто, то скрипт возьмёт имя из email) 136 | username: '' 137 | }, 138 | } 139 | }, 140 | sites: [ 141 | { 142 | host: ['grabfreegame.com', 'bananatic.com', 'gamingimpact.com'], 143 | element: 'a[href*="logout"]', 144 | steamKeys: '.code:visible', 145 | conditions: [ 146 | { 147 | element: '.tasks li:has(.banicon-steam2)', 148 | steamGroups() { 149 | const tasks = $J('.tasks li:has(.banicon-steam2) .buttons button:first-child'); 150 | const groups = []; 151 | 152 | for (const task of tasks) { 153 | const val = $J(task).attr('onclick'); 154 | 155 | if (val) { 156 | const url = val.match(/'(.+)'/); 157 | 158 | if (url) { 159 | groups.push(url[1]); 160 | } 161 | } 162 | } 163 | 164 | return groups; 165 | } 166 | }, 167 | { 168 | element: '.tasks li:not(:has(.completed)):not(:has(.banicon-twitter)):not(.tasks li:has(.banicon-youtube):not(:has([data-ytid])))', 169 | buttons: [ 170 | { 171 | type: 'tasks', 172 | cancellable: true, 173 | click(params) { 174 | const tasks = $J(params.self.element); 175 | 176 | log.debug(`tasks found : ${tasks.length}`); 177 | 178 | if (tasks.length) { 179 | return new Promise(async (resolve) => { 180 | let done = 0; 181 | 182 | for (let i = 0; i < tasks.length; i++) { 183 | if (params.cancelled) { 184 | log.debug('cancelled'); 185 | 186 | break; 187 | } 188 | 189 | const buttons = $J(tasks.get(i)).find('.buttons button'); 190 | 191 | if (buttons.length < 2) { 192 | continue; 193 | } 194 | 195 | if (!$J(buttons.get(0)).attr('data-ytid')) { 196 | const val = $J(buttons.get(0)).attr('onclick'); 197 | 198 | if (val) { 199 | let url = val.match(/'(.+)'/); 200 | 201 | if (!url) { 202 | log.debug(`${i + 1} : cannot extract url : ${val}`); 203 | 204 | continue; 205 | } 206 | 207 | url = url[1]; 208 | 209 | log.debug(`${i + 1} : making action request to ${url}`); 210 | 211 | try { 212 | await $J.get(url); 213 | } catch (e) {} 214 | } 215 | } 216 | 217 | const val = $J(buttons.get(1)).attr('onclick'); 218 | if (val) { 219 | let url = val.match(/'(.+)'/); 220 | 221 | if (!url) { 222 | log.debug(`${i + 1} : cannot extract url : ${val}`); 223 | 224 | continue; 225 | } 226 | 227 | url = url[1]; 228 | 229 | log.debug(`${i + 1} : making verify request to ${url}`); 230 | 231 | try { 232 | await $J.get(url); 233 | } catch (e) {} 234 | } 235 | 236 | params.button.progress(tasks.length, i + 1); 237 | 238 | done++; 239 | 240 | log.debug(`${i + 1} : done`); 241 | } 242 | 243 | if (done) { 244 | notifications.success(i18n.get('reload-to-see-changes'), {timeout: 0}); 245 | } 246 | 247 | resolve(); 248 | }); 249 | } 250 | } 251 | } 252 | ] 253 | } 254 | ] 255 | }, 256 | { 257 | host: 'whosgamingnow.net', 258 | element: 'a[href*="logout"]', 259 | steamKeys: '.SteamKey:visible', 260 | steamGroups: '.action[href*="steamcommunity.com/groups/"]', 261 | conditions: [ 262 | { 263 | element: '.action:not(:has(.fa-check-square-o))', 264 | buttons: [ 265 | { 266 | type: 'tasks', 267 | click(params) { 268 | const tasks = unsafeWindow.$(params.self.element); 269 | 270 | log.debug(`tasks found : ${tasks.length}`); 271 | 272 | tasks.trigger('click'); 273 | } 274 | } 275 | ] 276 | } 277 | ] 278 | }, 279 | { 280 | host: 'gamehag.com', 281 | console: true, 282 | element: '!#login-tools', 283 | steamKeys: '.response-key .code-text:visible', 284 | conditions: [ 285 | { 286 | path: /\/giveaway\//, 287 | steamKeys: '.giveaway-key input:visible%val', 288 | steamGroups() { 289 | const groups = []; 290 | 291 | $J('.single-giveaway-task').each((i, el) => { 292 | const href = $J(el).find('.task-icon use').attr('xlink:href'); 293 | 294 | if (href && href.includes('nc-logo-steam')) { 295 | const url = $J(el).find('.task-actions a').attr('href'); 296 | 297 | if (url) { 298 | groups.push(url); 299 | } 300 | } 301 | }); 302 | 303 | return groups; 304 | }, 305 | conditions: [ 306 | { 307 | elementAnd: ['.single-giveaway-task:has(.notdone):has(.task-actions a):has(.task-actions button),.single-giveaway-task:has(.notdone):has(.task-actions .giveaway-survey)', '!.giveaway-content .alert-danger:visible', '!.giveaway-key input:visible'], 308 | buttons: [ 309 | { 310 | type: 'tasks', 311 | cancellable: true, 312 | click(params) { 313 | const tasks = $J(params.self.elementAnd[0]); 314 | 315 | log.debug(`tasks found : ${tasks.length}`); 316 | 317 | if (tasks.length) { 318 | return new Promise((resolve) => { 319 | const completeTask = async (el) => { 320 | const task = $J(el); 321 | const action = task.find('.task-actions a'); 322 | const verify = task.find('.task-actions button'); 323 | 324 | if (action.length && verify.length) { 325 | let href = action.attr('href'); 326 | 327 | log.debug(`${i + 1} : completeTask() : making action request : ${href}`); 328 | 329 | try { 330 | let response = await $J.get(href); 331 | let lnk = $J(response.content).find('.game-list .col:not(:first-child) .game-tile .actions .btn-primary'); 332 | 333 | if (lnk.length) { 334 | href = lnk.attr('href'); 335 | 336 | log.debug(`${i + 1} : completeTask() : it looks like a "play game" task, making "/play" request : ${href}`); 337 | 338 | response = await $J.get(href); 339 | 340 | log.debug(`${i + 1} : completeTask() : "/play" request done`); 341 | 342 | lnk = $J(response).find('#single-game-play'); 343 | 344 | if (lnk.length) { 345 | href = lnk.attr('href'); 346 | 347 | log.debug(`${i + 1} : completeTask() : making "/redirect" request : ${href}`); 348 | 349 | await $J.get(href); 350 | 351 | log.debug(`${i + 1} : completeTask() : "/redirect" request done`); 352 | } 353 | } 354 | } catch (e) {} 355 | 356 | log.debug(`${i + 1} : completeTask() : clicking verify button...`); 357 | 358 | verify.trigger('click'); 359 | } else { 360 | const survey = task.find('.task-actions .giveaway-survey'); 361 | 362 | if (survey.length) { 363 | const id = survey.attr('data-task_id'); 364 | 365 | if (id) { 366 | log.debug(`${i + 1} : completeTask() : survey task : ${id}`); 367 | 368 | unsafeWindow.currentSurveyId = id; 369 | unsafeWindow.giveawaySurvCompleted(); 370 | } 371 | } 372 | } 373 | }; 374 | 375 | let i = 0; 376 | 377 | const ajaxComplete = (e, xhr, settings) => { 378 | if (settings.url.includes('/giveaway/sendtask')) { 379 | log.debug(`${i + 1} : ajaxComplete() : /giveaway/sendtask`); 380 | 381 | i++; 382 | params.button.progress(tasks.length, i); 383 | 384 | if (i >= tasks.length) { 385 | log.debug('all tasks done'); 386 | 387 | unsafeWindow.$(document).unbind('ajaxComplete', ajaxComplete); 388 | return resolve(); 389 | } 390 | 391 | if (params.cancelled) { 392 | log.debug('cancelled'); 393 | 394 | unsafeWindow.$(document).unbind('ajaxComplete', ajaxComplete); 395 | return resolve(); 396 | } 397 | 398 | completeTask(tasks.get(i)); 399 | } 400 | }; 401 | 402 | unsafeWindow.$(document).ajaxComplete(ajaxComplete); 403 | 404 | completeTask(tasks.get(i)); 405 | }); 406 | } 407 | } 408 | } 409 | ] 410 | } 411 | ] 412 | } 413 | ] 414 | }, 415 | { 416 | host: 'gleam.io', 417 | check(params) { 418 | const container = $J('.popup-blocks-container'); 419 | 420 | if (!container.length) { 421 | return false; 422 | } 423 | 424 | params.self._gleam = unsafeWindow.angular.element(container.get(0)).scope(); 425 | return !!params.self._gleam; 426 | }, 427 | ready(params) { 428 | for (const entry of params.self._gleam.entry_methods) { 429 | if (params.self._gleam.isTimerAction(entry)) { 430 | entry.timePassed = true; 431 | } 432 | } 433 | }, 434 | steamKeys(params) { 435 | return params.self._gleam.bestCouponCode(); 436 | }, 437 | steamGroups(params) { 438 | const groups = []; 439 | 440 | for (const entry of params.self._gleam.entry_methods) { 441 | if (entry.entry_type === 'steam_join_group') { 442 | groups.push(entry.config3); 443 | } 444 | } 445 | 446 | return groups; 447 | }, 448 | conditions: [ 449 | { 450 | getEntries(params) { 451 | return $J('.entry-method:visible:not(.completed-entry-method)').filter((i, e) => { 452 | const scope = unsafeWindow.angular.element(e).scope(); 453 | 454 | return scope && 455 | params.site._gleam.canEnter(scope.entry_method) && 456 | !params.site._gleam.isEntered(scope.entry_method) && 457 | params.site._gleam.enoughUserDetails(scope.entry_method) && 458 | (/(custom_action|_view|_visit|blog_comment|twitter_tweet|twitter_retweet|twitter_follow|tiktok_visit|tiktok_follow)/.test(scope.entry_method.entry_type) || ( 459 | params.site._gleam.enoughEntryDetails(scope.entry_method) && 460 | (!scope.entry_method.requires_authentication || params.site._gleam.isAuthenticated(scope.entry_method, scope.entry_method.provider)) 461 | )); 462 | }); 463 | }, 464 | check(params) { 465 | return params.site._gleam.contestantState.contestant.id && 466 | !params.site._gleam.bestCouponCode() && 467 | params.self.getEntries(params).length; 468 | }, 469 | buttons: [ 470 | { 471 | type: 'tasks', 472 | cancellable: true, 473 | click(params) { 474 | const tasks = params.self.getEntries(params); 475 | 476 | log.debug(`tasks found : ${tasks.length}`); 477 | 478 | if (tasks.length) { 479 | return new Promise(async (resolve) => { 480 | const emailName = params.site._gleam.contestantState.contestant.email.match(/[^@]+/)[0].replace(/[\.\+]/g, '_').slice(-15); 481 | 482 | for (let i = 0; i < tasks.length; i++) { 483 | if (params.cancelled) { 484 | log.debug('cancelled'); 485 | 486 | return resolve(); 487 | } 488 | if (params.site._gleam.bestCouponCode()) { 489 | log.debug('key is available'); 490 | 491 | return resolve(); 492 | } 493 | 494 | const scope = unsafeWindow.angular.element(tasks[i]).scope(); 495 | 496 | try { 497 | if (/(custom_action|_view|_visit|blog_comment|tiktok_visit|tiktok_follow)/.test(scope.entry_method.entry_type)) { 498 | log.debug(`${i + 1} : visit :`, scope.entry_method.action_description); 499 | 500 | params.site._gleam.triggerVisit(scope.entry_method); 501 | 502 | await utils.sleep(300); 503 | } 504 | 505 | if ((scope.entry_method.entry_type === 'custom_action' && /(Ask a question|Allow question or tracking)/.test(scope.entry_method.method_type)) || 506 | scope.entry_method.entry_type === 'blog_comment' || scope.entry_method.config3 === 'Question' || scope.entry_method.config5 === 'Question' 507 | ) { 508 | let answer; 509 | 510 | if (scope.entry_method.config5 === '1') { 511 | if (config.sitesConfig.gleam.answerQuestionsWithCheck.enabled) { 512 | answer = decodeURIComponent(atob(scope.entry_method.config8).replace(/\+/g, ' ')).split(/\r|\n/).find((v) => !!v); 513 | } 514 | } else if (config.sitesConfig.gleam.answerQuestions.enabled && config.sitesConfig.gleam.answerQuestions.answer) { 515 | answer = config.sitesConfig.gleam.answerQuestions.answer; 516 | } 517 | 518 | if (answer) { 519 | log.debug(`${i + 1} : details :`, answer, ':', scope.entry_method.action_description); 520 | 521 | params.site._gleam.entryDetailsState[scope.entry_method.id] = answer; 522 | params.site._gleam.entryState.formData[scope.entry_method.id] = answer; 523 | } 524 | } 525 | 526 | if (scope.entry_method.entry_type === 'tiktok_follow' && config.sitesConfig.gleam.tiktokSetUsername.enabled && 527 | !params.site._gleam.entryState.formData[scope.entry_method.id] 528 | ) { 529 | const tiktokName = config.sitesConfig.gleam.tiktokSetUsername.username ? config.sitesConfig.gleam.tiktokSetUsername.username : emailName; 530 | 531 | log.debug(`${i + 1} : tiktok username :`, tiktokName, ':', scope.entry_method.action_description); 532 | 533 | params.site._gleam.entryDetailsState[scope.entry_method.id] = tiktokName; 534 | params.site._gleam.entryState.formData[scope.entry_method.id] = tiktokName; 535 | } 536 | 537 | if (/(twitter_tweet|twitter_retweet|twitter_follow)/.test(scope.entry_method.entry_type)) { 538 | log.debug(`${i + 1} : twitter attempt :`, scope.entry_method.action_description); 539 | 540 | params.site._gleam.attemptEntry(scope.entry_method); 541 | 542 | await utils.sleep(200); 543 | 544 | if (config.sitesConfig.gleam.twitterSetUsername.enabled && !params.site._gleam.entryState.formData[scope.entry_method.id]) { 545 | const twitterName = config.sitesConfig.gleam.twitterSetUsername.username ? config.sitesConfig.gleam.twitterSetUsername.username : emailName; 546 | 547 | log.debug(`${i + 1} : twitter username :`, twitterName, ':', scope.entry_method.action_description); 548 | 549 | scope.entry_method.show_extra = true; 550 | params.site._gleam.entryDetailsState[scope.entry_method.id] = {twitter_username: twitterName}; 551 | params.site._gleam.entryState.formData[scope.entry_method.id] = {twitter_username: twitterName}; 552 | } 553 | } 554 | 555 | if (params.site._gleam.enoughEntryDetails(scope.entry_method) && 556 | (!scope.entry_method.requires_authentication || params.site._gleam.isAuthenticated(scope.entry_method, scope.entry_method.provider)) 557 | ) { 558 | log.debug(`${i + 1} : confirm :`, scope.entry_method.action_description); 559 | 560 | params.site._gleam.resumeEntry(scope.entry_method); 561 | 562 | while (scope.entry_method.entering) { 563 | await utils.sleep(500); 564 | } 565 | } 566 | } catch (e) { 567 | log.debug(`${i + 1} : task error :`, e); 568 | } 569 | 570 | params.button.progress(tasks.length, i + 1); 571 | } 572 | 573 | log.debug('all tasks done'); 574 | 575 | resolve(); 576 | }); 577 | } 578 | } 579 | } 580 | ] 581 | } 582 | ] 583 | }, 584 | { 585 | host: 'giveaway.su', 586 | element: 'a[href*="logout"]', 587 | steamKeys: '.giveaway-key input%val', 588 | steamGroups() { 589 | const groups = []; 590 | 591 | $J('#actions tr:has(.fa-steam-symbol)').each((i, el) => { 592 | const btn = $J(el).find('button[data-type="action.universal"]'); 593 | 594 | if (btn.length) { 595 | const action = JSON.parse(window.atob(btn.attr('data-action'))); 596 | 597 | if (action.task.includes('steamcommunity.com/groups/') || action.task.includes('bit.ly/')) { 598 | groups.push(action.task); 599 | } 600 | } 601 | }); 602 | 603 | return groups; 604 | } 605 | }, 606 | { 607 | host: 'keyjoker.com', 608 | element: 'a[href*="logout"]', 609 | conditions: [ 610 | { 611 | path: /^\/entries/, 612 | steamGroups: '.list-complete-item:has(.fa-steam) a.btn-primary', 613 | conditions: [ 614 | { 615 | element: '.list-complete-item button', 616 | buttons: [ 617 | { 618 | type: 'tasks', 619 | cancellable: true, 620 | click(params) { 621 | const tasks = $J(params.self.element); 622 | 623 | log.debug(`tasks found : ${tasks.length}`); 624 | 625 | if (tasks.length) { 626 | return new Promise(async (resolve) => { 627 | document.cookie = 'fraud_warning_notice=1; expires=Sun, 1 Jan 2030 00:00:00 UTC; path=/'; 628 | 629 | for (let i = 0; i < tasks.length; i++) { 630 | if (params.cancelled) { 631 | break; 632 | } 633 | 634 | log.debug(`${i + 1} : click`); 635 | 636 | tasks.get(i).click(); 637 | 638 | await utils.sleep(200); 639 | await utils.waitForElement('.list-complete-item button .spinner-border', false); 640 | 641 | log.debug(`${i + 1} : task done`); 642 | 643 | params.button.progress(tasks.length, i + 1); 644 | } 645 | 646 | if (params.cancelled) { 647 | log.debug('cancelled'); 648 | } else { 649 | log.debug('all tasks done'); 650 | } 651 | 652 | resolve(); 653 | }); 654 | } 655 | } 656 | } 657 | ] 658 | } 659 | ] 660 | }, 661 | { 662 | path: /^\/account\/keys/, 663 | steamKeys: '[id^="key-"]', 664 | ready() { 665 | $J('.card-body .col-auto img').on('click', (e) => { 666 | const key = steam.extractKeys($J(e.currentTarget).parents('.card-body').find('[id^="key-"]').text()); 667 | 668 | if (key) { 669 | steam.openKeyActivationPage(key[0]); 670 | } 671 | }); 672 | 673 | $J('head').append( 674 | `` 679 | ); 680 | } 681 | } 682 | ] 683 | }, 684 | { 685 | host: 'key-hub.eu', 686 | element: 'a[href*="logout"]', 687 | steamGroups: ['.task a[href*="steamcommunity.com/gid/"]', '.task a[href*="steamcommunity.com/groups/"]'], 688 | steamAppWishlist: '.task a[href*="store.steampowered.com/app/"]', 689 | conditions: [ 690 | { 691 | element: '.task:not(:has(.task-result.fa-check-circle[style*="display: flex"])) a[href*="/away?data="]', 692 | buttons: [ 693 | { 694 | type: 'tasks', 695 | cancellable: true, 696 | click(params) { 697 | const tasks = $J(params.self.element); 698 | 699 | log.debug(`tasks found : ${tasks.length}`); 700 | 701 | if (tasks.length) { 702 | return new Promise(async (resolve) => { 703 | for (let i = 0; i < tasks.length; i++) { 704 | if (params.cancelled) { 705 | break; 706 | } 707 | 708 | const url = $J(tasks.get(i)).attr('href'); 709 | 710 | log.debug(`${i + 1} : making request : ${url}`); 711 | 712 | try { 713 | await $J.get(url); 714 | } catch (e) {} 715 | 716 | log.debug(`${i + 1} : task done`); 717 | 718 | params.button.progress(tasks.length, i + 1); 719 | } 720 | 721 | if (params.cancelled) { 722 | log.debug('cancelled'); 723 | } else { 724 | log.debug('all tasks done'); 725 | } 726 | 727 | resolve(); 728 | }); 729 | } 730 | } 731 | } 732 | ] 733 | } 734 | ] 735 | }, 736 | { 737 | host: 'givee.club', 738 | element: 'a[href*="logout"]', 739 | steamGroups: '.event-actions tr:has(.fa-steam-symbol) .event-action-label a:not([href*="#"])', 740 | steamAppWishlist: ['.event-actions tr:has(.fa-plus-circle) .event-action-label a:not([href*="#"])', '.event-actions tr:has(.fa-plus-circle) .event-action-label a[href="#"]@data-steam-wishlist-appid'], 741 | steamAppFollow: '.event-actions tr:has(.fa-heart) .event-action-label a:not([href*="#"])', 742 | conditions: [ 743 | { 744 | element: '.event-action-buttons .glyphicon-refresh', 745 | buttons: [ 746 | { 747 | type: 'tasks', 748 | cancellable: true, 749 | click(params) { 750 | const tasks = $J(params.self.element); 751 | 752 | log.debug(`tasks found : ${tasks.length}`); 753 | 754 | if (tasks.length) { 755 | return new Promise(async (resolve) => { 756 | for (let i = 0; i < tasks.length; i++) { 757 | if (params.cancelled) { 758 | break; 759 | } 760 | 761 | log.debug(`${i + 1} : click`); 762 | 763 | const task = tasks.get(i); 764 | 765 | task.click(); 766 | await utils.waitForElement('.event-action-checking .glyphicon-refresh', false, false, task.parent); 767 | 768 | log.debug(`${i + 1} : task done`); 769 | 770 | params.button.progress(tasks.length, i + 1); 771 | } 772 | 773 | if (params.cancelled) { 774 | log.debug('cancelled'); 775 | } else { 776 | log.debug('all tasks done'); 777 | } 778 | 779 | resolve(); 780 | }); 781 | } 782 | } 783 | } 784 | ] 785 | } 786 | ] 787 | }, 788 | { 789 | host: 'opquests.com', 790 | element: 'form[action*="logout"]', 791 | conditions: [ 792 | { 793 | path: /^\/quests\//, 794 | steamGroups: '.items-center:has(.fa-users):has(.submit-loader) a[href*="steamcommunity.com/groups/"]', 795 | steamAppWishlist: '.items-center:has(.fa-list):has(.submit-loader) a[href*="store.steampowered.com/app/"]', 796 | steamAppFollow: '.items-center:has(.fa-gamepad):has(.submit-loader) a[href*="store.steampowered.com/app/"]', 797 | steamAppAddToLibrary: '.items-center:has(.fa-plus-square):has(.submit-loader) a[href*="store.steampowered.com/app/"]', 798 | conditions: [ 799 | { 800 | element: '.items-center .submit-loader', 801 | buttons: [ 802 | { 803 | type: 'tasks', 804 | cancellable: true, 805 | click(params) { 806 | const tasks = $J(params.self.element); 807 | 808 | log.debug(`tasks found : ${tasks.length}`); 809 | 810 | if (tasks.length) { 811 | return new Promise(async (resolve) => { 812 | const m = window.location.pathname.match(/\/quests\/(\d+)/); 813 | 814 | log.debug('making confirm request'); 815 | 816 | try { 817 | await $J.get(`https://opquests.com/quests/${m[1]}?confirm=1`); 818 | } catch (e) {} 819 | 820 | let done = 0; 821 | 822 | for (let i = 0; i < tasks.length; i++) { 823 | if (params.cancelled) { 824 | break; 825 | } 826 | 827 | const task = $J(tasks.get(i)); 828 | const token = task.parent().find('input[name="_token"]').val(); 829 | const taskId = task.parent().find('input[name="task_id"]').val(); 830 | 831 | log.debug(`${i + 1} : making request`); 832 | 833 | try { 834 | await $J.post('https://opquests.com/entries', {_token: token, task_id: taskId}); 835 | } catch (e) {} 836 | 837 | params.button.progress(tasks.length, i + 1); 838 | 839 | done++; 840 | 841 | log.debug(`${i + 1} : done`); 842 | } 843 | 844 | if (params.cancelled) { 845 | log.debug('cancelled'); 846 | } else { 847 | log.debug('all tasks done'); 848 | } 849 | 850 | if (done) { 851 | notifications.success(i18n.get('reload-to-see-changes'), {timeout: 0}); 852 | } 853 | 854 | resolve(); 855 | }); 856 | } 857 | } 858 | } 859 | ] 860 | } 861 | ] 862 | }, 863 | { 864 | path: /^\/keys/, 865 | steamKeys: ['button[data-clipboard-text]@data-clipboard-text', '.w-full .mb-2'], 866 | ready() { 867 | const imgs = $J('.items-center .p-4 img'); 868 | 869 | if (imgs.length) { 870 | imgs.on('click', (e) => { 871 | steam.openKeyActivationPage($J(e.currentTarget).parents('.items-center').find('.mb-2:nth-child(2)').text().trim()); 872 | }); 873 | 874 | $J('head').append( 875 | `` 880 | ); 881 | } 882 | } 883 | }, 884 | ] 885 | } 886 | ] 887 | }; 888 | 889 | // https://materialdesignicons.com 890 | const icons = { 891 | checkmark: '', 892 | key: '', 893 | group: '', 894 | wishlist: '', 895 | follow: '', 896 | library: '', 897 | cancel: '', 898 | success: '', 899 | info: '', 900 | info2: '', 901 | warning: '', 902 | error: '', 903 | close: '' 904 | }; 905 | 906 | const i18n = { 907 | format(str, vars) { 908 | const replaceStringIds = (s) => { 909 | return s.replace(/\[([a-z0-9\-]+)\]/g, (m, id) => { 910 | return this.lang[id] ? replaceStringIds(this.lang[id]) : m; 911 | }); 912 | }; 913 | 914 | str = replaceStringIds(str); 915 | 916 | if (typeof vars === 'object') { 917 | Object.keys(vars).forEach((item) => { 918 | str = str.replace(new RegExp(`{${item}}`, 'g'), utils.encodeEntities(vars[item])); 919 | }); 920 | } 921 | 922 | return str; 923 | }, 924 | get(id, vars) { 925 | if (!this.lang) { 926 | return; 927 | } 928 | 929 | let res = this.lang[id]; 930 | 931 | if (typeof res === 'undefined' && this.lang !== this.langs.default) { 932 | res = this.langs.default[id]; 933 | } 934 | 935 | return this.format(res, vars); 936 | }, 937 | lang: null, 938 | langs: { 939 | default: { 940 | 'confirm-tasks': 'Confirm tasks', 941 | 'cancel': 'Cancel', 942 | 'useful-info': 'Useful information', 943 | 'reload-to-see-changes': 'Reload the page to see changes.', 944 | 'steam-activate-key': 'Open Steam key activation page ({key})', 945 | 'steam-loading-tasks': 'Loading Steam tasks...', 946 | 'steam-group-join': 'Join Steam group "{group}" (Ctrl+Click - open the group in a new tab)', 947 | 'steam-group-leave': 'Leave Steam group "{group}" (Ctrl+Click - open the group in a new tab)', 948 | 'steam-init-groups-request-failed': 'Failed to load your groups. Steam Community is probably down (check Steam Status).', 949 | 'steam-init-store-request-failed': 'Failed to get information from your Steam account. Steam Store is probably down (check Steam Status).', 950 | 'steam-join-group-failed': 'Failed to join the group. Steam Community is probably experiencing some issues (check Steam Status).', 951 | 'steam-join-group-join-request-sent': 'Join request sent. To join the group, your join request must be approved by the group administrator.', 952 | 'steam-join-group-not-logged': 'Failed to join the group. [steam-community-not-logged]', 953 | 'steam-join-group-not-found': 'Failed to join the group. Looks like the group does not exist.', 954 | 'steam-leave-group-failed': 'Failed to leave the group. Steam Community is probably experiencing some issues (check Steam Status).', 955 | 'steam-leave-group-not-logged': 'Failed to leave the group. [steam-community-not-logged]', 956 | 'steam-app-wishlist-add': 'Add game #{appId} to Steam wishlist (Ctrl+Click - open the game in a new tab)', 957 | 'steam-app-wishlist-remove': 'Remove game #{appId} from Steam wishlist (Ctrl+Click - open the game in a new tab)', 958 | 'steam-app-wishlist-add-failed': 'Failed to add to Steam wishlist. [steam-store-issues]', 959 | 'steam-app-wishlist-remove-failed': 'Failed to remove from Steam wishlist. [steam-store-issues]', 960 | 'steam-app-follow': 'Follow game #{appId} on Steam (Ctrl+Click - open the game in a new tab)', 961 | 'steam-app-unfollow': 'Unfollow game #{appId} on Steam (Ctrl+Click - open the game in a new tab)', 962 | 'steam-app-follow-failed': 'Failed to follow a game on Steam. [steam-store-issues]', 963 | 'steam-app-unfollow-failed': 'Failed to unfollow a game on Steam. [steam-store-issues]', 964 | 'steam-app-add-to-library': 'Add game #{appId} to Steam library (Ctrl+Click - open the game in a new tab)', 965 | 'steam-app-add-to-library-failed': 'Failed to add a game to Steam library. [steam-store-issues]', 966 | 'steam-community-not-logged': 'It looks like you are not logged in to Steam Community.', 967 | 'steam-store-not-logged': 'It looks like you are not logged in to Steam Store.', 968 | 'steam-store-issues': 'Perhaps you are not logged in to Steam Store or Steam is experiencing some issues (check Steam Status).', 969 | 'gc-updated': `Giveaway Companion has been updated to version ${version.string}.

Changes:` 970 | }, 971 | ru: { 972 | 'confirm-tasks': 'Подтвердить задания', 973 | 'cancel': 'Отмена', 974 | 'useful-info': 'Полезная информация', 975 | 'reload-to-see-changes': 'Обновите страницу, чтобы увидеть изменения.', 976 | 'steam-activate-key': 'Открыть страницу активации Steam ключа ({key})', 977 | 'steam-loading-tasks': 'Загрузка заданий Steam...', 978 | 'steam-group-join': 'Вступить в Steam группу "{group}" (Ctrl+Клик - открыть группу в новой вкладке)', 979 | 'steam-group-leave': 'Выйти из Steam группы "{group}" (Ctrl+Клик - открыть группу в новой вкладке)', 980 | 'steam-init-groups-request-failed': 'Не удалось загрузить ваши группы. Сообщество Steam, возможно, неактивно (проверьте Steam Status).', 981 | 'steam-init-store-request-failed': 'Не удалось загрузить информацию из вашего аккаунта Steam. Магазин Steam, возможно, неактивен (проверьте Steam Status).', 982 | 'steam-join-group-failed': 'Не удалось вступить в группу. Сообщество Steam, возможно, испытывает какие-то проблемы (проверьте Steam Status).', 983 | 'steam-join-group-join-request-sent': 'Заявка на вступление отправлена. Чтобы вступить в группу, вашу заявку должен одобрить администратор группы.', 984 | 'steam-join-group-not-logged': 'Не удалось вступить в группу. [steam-community-not-logged]', 985 | 'steam-join-group-not-found': 'Не удалось вступить в группу. Похоже, группа не существует.', 986 | 'steam-leave-group-failed': 'Не удалось выйти из группы. Сообщество Steam, возможно, испытывает какие-то проблемы (проверьте Steam Status).', 987 | 'steam-leave-group-not-logged': 'Не удалось выйти из группы. [steam-community-not-logged]', 988 | 'steam-app-wishlist-add': 'Добавить в список желаемого Steam игру #{appId} (Ctrl+Клик - открыть игру в новой вкладке)', 989 | 'steam-app-wishlist-remove': 'Удалить из списка желаемого Steam игру #{appId} (Ctrl+Клик - открыть игру в новой вкладке)', 990 | 'steam-app-wishlist-add-failed': 'Не удалось добавить в список желаемого Steam. [steam-store-issues]', 991 | 'steam-app-wishlist-remove-failed': 'Не удалось удалить из списка желаемого Steam. [steam-store-issues]', 992 | 'steam-app-follow': 'Подписаться в Steam на игру #{appId} (Ctrl+Клик - открыть игру в новой вкладке)', 993 | 'steam-app-unfollow': 'Отписаться в Steam от игры #{appId} (Ctrl+Клик - открыть игру в новой вкладке)', 994 | 'steam-app-follow-failed': 'Не удалось подписаться на Steam игру. [steam-store-issues]', 995 | 'steam-app-unfollow-failed': 'Не удалось отписаться от Steam игры. [steam-store-issues]', 996 | 'steam-app-add-to-library': 'Добавить игру #{appId} в библиотеку Steam (Ctrl+Клик - открыть игру в новой вкладке)', 997 | 'steam-app-add-to-library-failed': 'Не удалось добавить игру в библиотеку Steam. [steam-store-issues]', 998 | 'steam-community-not-logged': 'Похоже, вы не авторизованы в Сообществе Steam.', 999 | 'steam-store-not-logged': 'Похоже, вы не авторизованы в Магазине Steam.', 1000 | 'steam-store-issues': 'Возможно, вы не авторизованы в Магазине Steam или Steam испытывает какие-то проблемы (проверьте Steam Status).', 1001 | 'gc-updated': `Giveaway Companion был обновлён до версии ${version.string}.

Изменения:` 1002 | } 1003 | } 1004 | }; 1005 | 1006 | const state = { 1007 | host: window.location.hostname.replace(/^www\./, ''), 1008 | site: null 1009 | }; 1010 | 1011 | const utils = { 1012 | _resolvedUrl: {}, 1013 | randomString(length) { 1014 | const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 1015 | let result = ''; 1016 | 1017 | for (let i = 0; i < length; i++) { 1018 | result += chars[Math.floor(Math.random() * chars.length)]; 1019 | } 1020 | 1021 | return result; 1022 | }, 1023 | scrollTo(element) { 1024 | const el = $J(element); 1025 | 1026 | if (!el.length || !el.is(':visible')) { 1027 | return; 1028 | } 1029 | 1030 | $J('html, body').animate({ 1031 | scrollTop: parseInt(el.offset().top) 1032 | }, 500); 1033 | }, 1034 | async resolveUrl(url) { 1035 | log.debug(`utils.resolveUrl("${url}")`); 1036 | 1037 | const cached = this.getResolvedUrl(url); 1038 | 1039 | if (cached) { 1040 | log.debug(`utils.resolveUrl() : url found in the cache : ${cached}`); 1041 | 1042 | return cached; 1043 | } 1044 | 1045 | log.debug(`utils.resolveUrl() : making request : ${url}`); 1046 | 1047 | const response = await $GM.xmlHttpRequest({ 1048 | method: 'GET', 1049 | url: url 1050 | }); 1051 | 1052 | if (response.status !== 200) { 1053 | log.debug(`utils.resolveUrl() : request failed : ${url} : ${response.status}`); 1054 | 1055 | return false; 1056 | } 1057 | 1058 | this._resolvedUrl[url] = response.finalUrl; 1059 | 1060 | log.debug(`utils.resolveUrl() : final url : ${response.finalUrl}`); 1061 | 1062 | return response.finalUrl; 1063 | }, 1064 | getResolvedUrl(url) { 1065 | return this._resolvedUrl[url]; 1066 | }, 1067 | // https://github.com/angular/angular.js/blob/26a5779cddf70944b7548e3a6410d35237a516e5/src/ngSanitize/sanitize.js#L577 1068 | encodeEntities(value) { 1069 | const SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; 1070 | const NON_ALPHANUMERIC_REGEXP = /([^#-~ |!])/g; 1071 | return value. 1072 | replace(/&/g, '&'). 1073 | replace(SURROGATE_PAIR_REGEXP, function(value) { 1074 | const hi = value.charCodeAt(0); 1075 | const low = value.charCodeAt(1); 1076 | return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; 1077 | }). 1078 | replace(NON_ALPHANUMERIC_REGEXP, function(value) { 1079 | return '&#' + value.charCodeAt(0) + ';'; 1080 | }). 1081 | replace(//g, '>'); 1083 | }, 1084 | async waitForElement(selectors, waitForExistence = true, visible = false, parent = document, interval = 250, seconds = 0) { 1085 | const isVisible = (e) => { 1086 | return !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length); 1087 | }; 1088 | 1089 | return new Promise((resolve) => { 1090 | if (!Array.isArray(selectors)) { 1091 | selectors = [selectors]; 1092 | } 1093 | 1094 | seconds = seconds * 1000; 1095 | 1096 | const startTime = Date.now(); 1097 | const check = () => { 1098 | let found = true; 1099 | let el; 1100 | 1101 | for (const s of selectors) { 1102 | el = parent.querySelector(s); 1103 | 1104 | if ((waitForExistence && (!el || (visible && !isVisible(el)))) || (!waitForExistence && el)) { 1105 | found = false; 1106 | break; 1107 | } 1108 | } 1109 | 1110 | if (found) { 1111 | return resolve(el); 1112 | } 1113 | 1114 | if (seconds > 0 && Date.now() - startTime > seconds) { 1115 | return resolve(false); 1116 | } 1117 | 1118 | setTimeout(check, interval); 1119 | }; 1120 | 1121 | check(); 1122 | }); 1123 | }, 1124 | async sleep(ms) { 1125 | return new Promise((resolve) => { 1126 | setTimeout(resolve, ms); 1127 | }); 1128 | } 1129 | }; 1130 | 1131 | const overlay = { 1132 | _element: null, 1133 | visible(value) { 1134 | if (typeof value === 'undefined') { 1135 | return this._element !== null; 1136 | } 1137 | 1138 | if (value) { 1139 | this._element = $J(`
`) 1140 | .appendTo('body'); 1141 | } else { 1142 | if (this._element) { 1143 | this._element.remove(); 1144 | this._element = null; 1145 | } 1146 | } 1147 | } 1148 | }; 1149 | 1150 | const steam = { 1151 | initFailed: false, 1152 | _sessionId: null, 1153 | _userId: null, 1154 | _processUrl: null, 1155 | _userGroups: [], 1156 | _userWishlistedApps: [], 1157 | _userFollowedApps: [], 1158 | _userOwnedApps: [], 1159 | _idCache: {}, 1160 | _activateKeyUrl: 'https://store.steampowered.com/account/registerkey?key=', 1161 | _userGroupsUrl: 'https://steamcommunity.com/my/groups', 1162 | _userDataUrl: 'https://store.steampowered.com/dynamicstore/userdata/?v=275&id=', 1163 | _groupUrl: 'https://steamcommunity.com/groups/', 1164 | _appUrl: 'https://store.steampowered.com/app/', 1165 | _addToWishlistUrl: 'https://store.steampowered.com/api/addtowishlist', 1166 | _removeFromWishlistUrl: 'https://store.steampowered.com/api/removefromwishlist', 1167 | _followAppUrl: 'https://store.steampowered.com/explore/followgame/', 1168 | _addToLibraryUrl: 'https://store.steampowered.com/freelicense/addfreelicense/', 1169 | _groupRegex: /steamcommunity\.com\/groups\/([a-zA-Z0-9\-_]{2,32})/, 1170 | _appRegex: /store\.steampowered\.com\/.*?app(?:\/|%2F)(\d+)/, 1171 | _keyRegex: /[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}(-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5})?/g, 1172 | _initPromise: null, 1173 | openGroupPage(group) { 1174 | if (typeof group !== 'string') { 1175 | return; 1176 | } 1177 | 1178 | log.debug(`steam.openGroupPage("${group}")`); 1179 | 1180 | window.open(this._groupUrl + group, '_blank'); 1181 | }, 1182 | openKeyActivationPage(key) { 1183 | if (typeof key !== 'string') { 1184 | return; 1185 | } 1186 | 1187 | log.debug(`steam.activateKey("${key}")`); 1188 | 1189 | window.open(this._activateKeyUrl + key, '_blank'); 1190 | }, 1191 | openAppPage(id) { 1192 | if (typeof id !== 'string' && typeof id !== 'number') { 1193 | return; 1194 | } 1195 | 1196 | log.debug(`steam.openAppPage(${id})`); 1197 | 1198 | window.open(this._appUrl + id, '_blank'); 1199 | }, 1200 | extractKeys(txt) { 1201 | if (typeof txt !== 'string') { 1202 | return false; 1203 | } 1204 | 1205 | const match = txt.match(this._keyRegex); 1206 | 1207 | if (match) { 1208 | return match; 1209 | } 1210 | 1211 | return false; 1212 | }, 1213 | extractGroupName(url) { 1214 | if (typeof url !== 'string') { 1215 | return false; 1216 | } 1217 | 1218 | const match = url.match(this._groupRegex); 1219 | 1220 | if (match) { 1221 | return match[1]; 1222 | } 1223 | 1224 | return false; 1225 | }, 1226 | extractAppId(url) { 1227 | if (typeof url !== 'string') { 1228 | return false; 1229 | } 1230 | 1231 | const match = url.match(this._appRegex); 1232 | 1233 | if (match) { 1234 | return match[1]; 1235 | } 1236 | 1237 | return false; 1238 | }, 1239 | async init() { 1240 | if (!config.steamGroups || this.initFailed) { 1241 | return false; 1242 | } 1243 | if (this._sessionId) { 1244 | return true; 1245 | } 1246 | if (this._initPromise) { 1247 | return this._initPromise; 1248 | } 1249 | 1250 | this._initPromise = new Promise(async (resolve) => { 1251 | log.debug('steam.init()'); 1252 | log.debug(`steam.init() : making request : ${this._userGroupsUrl}`); 1253 | 1254 | let response; 1255 | 1256 | for (let i = 0; i < 2; i++) { 1257 | response = await $GM.xmlHttpRequest({ 1258 | method: 'GET', 1259 | url: this._userGroupsUrl 1260 | }); 1261 | 1262 | if (response.status !== 200) { 1263 | log.debug(`steam.init() : request failed : ${response.status}`); 1264 | 1265 | notifications.error(i18n.get('steam-init-groups-request-failed')); 1266 | 1267 | this.initFailed = true; 1268 | return resolve(false); 1269 | } 1270 | 1271 | if (response.finalUrl.includes('/login/')) { 1272 | log.debug('steam.init() : redirected to login'); 1273 | 1274 | if (i == 0) { 1275 | log.debug('steam.init() : making request : https://login.steampowered.com/jwt/refresh'); 1276 | 1277 | response = await $GM.xmlHttpRequest({ 1278 | method: 'GET', 1279 | url: 'https://login.steampowered.com/jwt/refresh?redir=https%3A%2F%2Fsteamcommunity.com%2Fmy%2Fgroups' 1280 | }); 1281 | } else { 1282 | log.debug('steam.init() : user not logged'); 1283 | 1284 | notifications.error(i18n.get('steam-community-not-logged')); 1285 | 1286 | this.initFailed = true; 1287 | return resolve(false); 1288 | } 1289 | } else { 1290 | break; 1291 | } 1292 | } 1293 | 1294 | const responseDom = $J(response.responseText); 1295 | const userUrl = responseDom.find('.friends_header_name a').attr('href'); 1296 | this._sessionId = response.responseText.match(/g_sessionID\s*=\s*"(.+?)"/); 1297 | 1298 | if (!userUrl || !this._sessionId) { 1299 | log.debug('steam.init() : user data not found'); 1300 | 1301 | notifications.error(i18n.get('steam-community-not-logged')); 1302 | 1303 | this.initFailed = true; 1304 | return resolve(false); 1305 | } 1306 | 1307 | this._processUrl = userUrl + '/home_process'; 1308 | this._sessionId = this._sessionId[1]; 1309 | 1310 | responseDom.find('.groupTitle a').each((i, e) => { 1311 | const m = this.extractGroupName($J(e).attr('href')); 1312 | 1313 | if (m) { 1314 | this._userGroups.push(m.toLowerCase()); 1315 | } 1316 | }); 1317 | 1318 | this._userId = responseDom.find('a[data-miniprofile]').attr('data-miniprofile'); 1319 | 1320 | log.debug(`steam.init() : making request : ${this._userDataUrl + this._userId}`); 1321 | 1322 | response = await $GM.xmlHttpRequest({ 1323 | method: 'GET', 1324 | url: this._userDataUrl + this._userId + '&t=' + Date.now() 1325 | }); 1326 | 1327 | if (response.status !== 200) { 1328 | log.debug(`steam.init() : request failed : ${response.status}`); 1329 | 1330 | notifications.error(i18n.get('steam-init-store-request-failed')); 1331 | 1332 | this.initFailed = true; 1333 | return resolve(false); 1334 | } 1335 | 1336 | const json = JSON.parse(response.responseText); 1337 | 1338 | this._userWishlistedApps = json.rgWishlist; 1339 | this._userFollowedApps = json.rgFollowedApps; 1340 | this._userOwnedApps = json.rgOwnedApps; 1341 | 1342 | log.debug('steam.init() : done'); 1343 | 1344 | resolve(true); 1345 | }); 1346 | 1347 | return this._initPromise; 1348 | }, 1349 | async joinGroup(groupName) { 1350 | if (!config.steamGroups || !this._sessionId) { 1351 | return false; 1352 | } 1353 | 1354 | groupName = groupName.toLowerCase(); 1355 | 1356 | log.debug(`steam.joinGroup(${groupName})`); 1357 | 1358 | let groupInfo = await this.getGroupInfo(groupName); 1359 | 1360 | if (!groupInfo) { 1361 | notifications.error(i18n.get('steam-join-group-failed', {'groupLink': this._groupUrl + groupName})); 1362 | 1363 | return false; 1364 | } 1365 | 1366 | log.debug(`steam.joinGroup() : making join request : ${this._groupUrl}${groupName}`); 1367 | 1368 | const response = await $GM.xmlHttpRequest({ 1369 | method: 'POST', 1370 | url: this._groupUrl + groupName, 1371 | headers: { 1372 | 'Content-Type': 'application/x-www-form-urlencoded', 1373 | 'Origin': 'https://steamcommunity.com' 1374 | }, 1375 | data: $J.param({action: 'join', sessionID: groupInfo.sessionId}) 1376 | }); 1377 | 1378 | if (response.status !== 200) { 1379 | log.debug(`steam.joinGroup() : request failed : ${response.status}`); 1380 | 1381 | notifications.error(i18n.get('steam-join-group-failed', {'groupLink': this._groupUrl + groupName})); 1382 | 1383 | return false; 1384 | } 1385 | 1386 | groupInfo = await this.getGroupInfo(groupName, response.responseText); 1387 | 1388 | if (!groupInfo) { 1389 | notifications.error(i18n.get('steam-join-group-failed', {'groupLink': this._groupUrl + groupName})); 1390 | 1391 | return false; 1392 | } 1393 | 1394 | if (groupInfo.status === 'joined') { 1395 | this._userGroups.push(groupName); 1396 | 1397 | return true; 1398 | } 1399 | 1400 | notifications.error(i18n.get('steam-join-group-failed', {'groupLink': this._groupUrl + groupName})); 1401 | 1402 | return false; 1403 | }, 1404 | async leaveGroup(groupName) { 1405 | if (!config.steamGroups || !this._sessionId || !this._processUrl) { 1406 | return false; 1407 | } 1408 | 1409 | groupName = groupName.toLowerCase(); 1410 | 1411 | log.debug(`steam.leaveGroup(${groupName})`); 1412 | 1413 | const groupInfo = await this.getGroupInfo(groupName); 1414 | 1415 | if (!groupInfo) { 1416 | notifications.error(i18n.get('steam-leave-group-failed', {'groupLink': this._groupUrl + groupName})); 1417 | 1418 | return false; 1419 | } 1420 | 1421 | if (groupInfo.status === 'not_logged' || groupInfo.status === 'wrong_page') { 1422 | notifications.error(i18n.get('steam-leave-group-not-logged', {'groupLink': this._groupUrl + groupName})); 1423 | 1424 | return false; 1425 | } else if (groupInfo.status === 'not_joined') { 1426 | return true; 1427 | } 1428 | 1429 | log.debug(`steam.leaveGroup() : making request : ${this._processUrl}`); 1430 | 1431 | const response = await $GM.xmlHttpRequest({ 1432 | method: 'POST', 1433 | url: this._processUrl, 1434 | headers: { 1435 | 'Content-Type': 'application/x-www-form-urlencoded', 1436 | 'Origin': 'https://steamcommunity.com' 1437 | }, 1438 | data: $J.param({action: 'leaveGroup', sessionID: groupInfo.sessionId, groupId: groupInfo.id}) 1439 | }); 1440 | 1441 | if (response.status !== 200) { 1442 | log.debug(`steam.leaveGroup() : request failed : ${response.status}`); 1443 | 1444 | notifications.error(i18n.get('steam-leave-group-failed', {'groupLink': this._groupUrl + groupName})); 1445 | 1446 | return false; 1447 | } 1448 | 1449 | if (!response.finalUrl.includes('/groups')) { 1450 | log.debug('steam.leaveGroup() : user is not logged in'); 1451 | 1452 | notifications.error(i18n.get('steam-leave-group-not-logged', {'groupLink': this._groupUrl + groupName})); 1453 | 1454 | return false; 1455 | } 1456 | 1457 | if (response.responseText.includes(groupInfo.id)) { 1458 | log.debug('steam.leaveGroup() : not left'); 1459 | 1460 | notifications.error(i18n.get('steam-leave-group-failed', {'groupLink': this._groupUrl + groupName})); 1461 | 1462 | return false; 1463 | } 1464 | 1465 | log.debug('steam.leaveGroup() : left'); 1466 | 1467 | const idx = this._userGroups.indexOf(groupName); 1468 | 1469 | if (idx !== -1) { 1470 | this._userGroups.splice(idx, 1); 1471 | } 1472 | 1473 | return true; 1474 | }, 1475 | async getGroupInfo(groupName, groupPage) { 1476 | let groupPageJ; 1477 | 1478 | groupName = groupName.toLowerCase(); 1479 | 1480 | if (groupPage) { 1481 | groupPageJ = $J(groupPage); 1482 | } else { 1483 | log.debug(`steam.getGroupInfo() : making request : ${this._groupUrl}${groupName}`); 1484 | 1485 | const response = await $GM.xmlHttpRequest({ 1486 | method: 'GET', 1487 | url: this._groupUrl + groupName 1488 | }); 1489 | 1490 | if (response.status !== 200) { 1491 | log.debug(`steam.getGroupInfo() : request failed : ${response.status}`); 1492 | 1493 | return false; 1494 | } 1495 | 1496 | groupPage = response.responseText; 1497 | groupPageJ = $J(response.responseText); 1498 | } 1499 | 1500 | const result = { 1501 | id: null, 1502 | sessionId: null 1503 | }; 1504 | 1505 | if (groupPageJ.find('.supernav_container').length) { 1506 | if (groupPageJ.find('#account_pulldown').length) { 1507 | if (groupPageJ.find('.grouppage_header_name').length) { 1508 | if (groupPageJ.find('a[href*="ConfirmLeaveGroup"]').length) { 1509 | log.debug('steam.getGroupInfo() : joined'); 1510 | 1511 | result.status = 'joined'; 1512 | } else { 1513 | if (groupPageJ.find('a[href*="ConfirmCancelJoinRequest"]').length) { 1514 | log.debug('steam.getGroupInfo() : waiting for approval'); 1515 | 1516 | result.status = 'approval'; 1517 | } else { 1518 | log.debug('steam.getGroupInfo() : not joined'); 1519 | 1520 | result.status = 'not_joined'; 1521 | } 1522 | } 1523 | } else { 1524 | log.debug('steam.getGroupInfo() : group not found'); 1525 | 1526 | result.status = 'not_found'; 1527 | } 1528 | } else { 1529 | log.debug('steam.getGroupInfo() : user is not logged in'); 1530 | 1531 | result.status = 'not_logged'; 1532 | } 1533 | } else { 1534 | log.debug('steam.getGroupInfo() : wrong page'); 1535 | 1536 | result.status = 'wrong_page'; 1537 | } 1538 | 1539 | const groupId = groupPageJ.find('input[name="groupId"]').val(); 1540 | 1541 | if (groupId) { 1542 | log.debug(`steam.getGroupInfo() : group id found : ${groupId}`); 1543 | 1544 | this._idCache[groupName] = groupId; 1545 | result.id = groupId; 1546 | } else { 1547 | log.debug('steam.getGroupInfo() : group id not found'); 1548 | } 1549 | 1550 | const sessionId = groupPage.match(/g_sessionID\s*=\s*"(.+?)"/); 1551 | 1552 | if (sessionId) { 1553 | log.debug(`steam.getGroupInfo() : g_sessionID found : ${sessionId[1]}`); 1554 | } else { 1555 | log.debug('steam.getGroupInfo() : g_sessionID not found'); 1556 | } 1557 | 1558 | result.sessionId = sessionId[1]; 1559 | 1560 | return result; 1561 | }, 1562 | async getSessionId(url) { 1563 | log.debug(`steam.getSessionId() : making request : ${url}`); 1564 | 1565 | let response = await $GM.xmlHttpRequest({ 1566 | method: 'GET', 1567 | url: url 1568 | }); 1569 | 1570 | if (response.status !== 200) { 1571 | log.debug(`steam.getSessionId() : request failed : ${response.status}`); 1572 | 1573 | return false; 1574 | } 1575 | 1576 | const sessionId = response.responseText.match(/g_sessionID\s*=\s*"(.+?)"/); 1577 | 1578 | if (!sessionId) { 1579 | log.debug('steam.getSessionId() : g_sessionID not found'); 1580 | 1581 | return false; 1582 | } 1583 | 1584 | return sessionId[1]; 1585 | }, 1586 | isJoinedGroup(groupName) { 1587 | if (!config.steamGroups) { 1588 | return false; 1589 | } 1590 | 1591 | groupName = groupName.toLowerCase(); 1592 | return this._userGroups.indexOf(groupName) !== -1; 1593 | }, 1594 | isAppWishlisted(appId) { 1595 | if (!config.steamAppWishlist) { 1596 | return false; 1597 | } 1598 | 1599 | appId = parseInt(appId); 1600 | 1601 | return this._userWishlistedApps.indexOf(appId) !== -1 || this._userOwnedApps.indexOf(appId) !== -1; 1602 | }, 1603 | isAppFollowed(appId) { 1604 | if (!config.steamAppFollow) { 1605 | return false; 1606 | } 1607 | 1608 | appId = parseInt(appId); 1609 | 1610 | return this._userFollowedApps.indexOf(appId) !== -1; 1611 | }, 1612 | isAppOwned(appId) { 1613 | appId = parseInt(appId); 1614 | 1615 | return this._userOwnedApps.indexOf(appId) !== -1; 1616 | }, 1617 | async addToWishlist(appId) { 1618 | if (!this._sessionId) { 1619 | return false; 1620 | } 1621 | 1622 | log.debug(`steam.addToWishlist(${appId})`); 1623 | 1624 | const sessionId = await this.getSessionId(`https://store.steampowered.com/app/${appId}/`); 1625 | 1626 | if (!sessionId) { 1627 | notifications.error(i18n.get('steam-store-not-logged')); 1628 | 1629 | return false; 1630 | } 1631 | 1632 | log.debug(`steam.addToWishlist() : making request : ${this._addToWishlistUrl}`); 1633 | 1634 | const response = await $GM.xmlHttpRequest({ 1635 | method: 'POST', 1636 | url: this._addToWishlistUrl, 1637 | headers: { 1638 | 'Content-Type': 'application/x-www-form-urlencoded', 1639 | 'Origin': 'https://store.steampowered.com', 1640 | 'Referer': `https://store.steampowered.com/app/${appId}/`, 1641 | 'X-Requested-With': 'XMLHttpRequest' 1642 | }, 1643 | data: $J.param({sessionid: sessionId, appid: appId}) 1644 | }); 1645 | 1646 | if (response.status !== 200) { 1647 | log.debug(`steam.addToWishlist() : request failed : ${response.status}`); 1648 | 1649 | notifications.error(i18n.get('steam-app-wishlist-add-failed')); 1650 | 1651 | return false; 1652 | } 1653 | 1654 | if (!JSON.parse(response.responseText).success) { 1655 | log.debug('steam.addToWishlist() : not success'); 1656 | 1657 | notifications.error(i18n.get('steam-app-wishlist-add-failed')); 1658 | 1659 | return false; 1660 | } 1661 | 1662 | this._userWishlistedApps.push(parseInt(appId)); 1663 | 1664 | return true; 1665 | }, 1666 | async removeFromWishlist(appId) { 1667 | if (!this._sessionId) { 1668 | return false; 1669 | } 1670 | 1671 | log.debug(`steam.removeFromWishlist(${appId})`); 1672 | 1673 | const sessionId = await this.getSessionId(`https://store.steampowered.com/app/${appId}/`); 1674 | 1675 | if (!sessionId) { 1676 | notifications.error(i18n.get('steam-store-not-logged')); 1677 | 1678 | return false; 1679 | } 1680 | 1681 | log.debug(`steam.removeFromWishlist() : making request : ${this._removeFromWishlistUrl}`); 1682 | 1683 | const response = await $GM.xmlHttpRequest({ 1684 | method: 'POST', 1685 | url: this._removeFromWishlistUrl, 1686 | headers: { 1687 | 'Content-Type': 'application/x-www-form-urlencoded', 1688 | 'Origin': 'https://store.steampowered.com' 1689 | }, 1690 | data: $J.param({sessionid: sessionId, appid: appId}) 1691 | }); 1692 | 1693 | if (response.status !== 200) { 1694 | log.debug(`steam.removeFromWishlist() : request failed : ${response.status}`); 1695 | 1696 | notifications.error(i18n.get('steam-app-wishlist-remove-failed')); 1697 | 1698 | return false; 1699 | } 1700 | 1701 | if (!JSON.parse(response.responseText).success) { 1702 | log.debug('steam.removeFromWishlist() : not success'); 1703 | 1704 | notifications.error(i18n.get('steam-app-wishlist-remove-failed')); 1705 | 1706 | return false; 1707 | } 1708 | 1709 | const index = this._userWishlistedApps.indexOf(parseInt(appId)); 1710 | 1711 | if (index !== -1) { 1712 | this._userWishlistedApps.splice(index, 1); 1713 | } 1714 | 1715 | return true; 1716 | }, 1717 | async followApp(appId) { 1718 | log.debug(`steam.followApp(${appId})`); 1719 | 1720 | const sessionId = await this.getSessionId(`https://store.steampowered.com/app/${appId}/`); 1721 | 1722 | if (!sessionId) { 1723 | notifications.error(i18n.get('steam-store-not-logged')); 1724 | 1725 | return false; 1726 | } 1727 | 1728 | log.debug(`steam.followApp() : making request : ${this._followAppUrl}`); 1729 | 1730 | const response = await $GM.xmlHttpRequest({ 1731 | method: 'POST', 1732 | url: this._followAppUrl, 1733 | headers: { 1734 | 'Content-Type': 'application/x-www-form-urlencoded', 1735 | 'Origin': 'https://store.steampowered.com' 1736 | }, 1737 | data: $J.param({sessionid: sessionId, appid: appId}) 1738 | }); 1739 | 1740 | if (response.status !== 200) { 1741 | log.debug(`steam.followApp() : request failed : ${response.status}`); 1742 | 1743 | notifications.error(i18n.get('steam-app-follow-failed')); 1744 | 1745 | return false; 1746 | } 1747 | 1748 | if (response.responseText != 'true') { 1749 | log.debug('steam.followApp() : not success'); 1750 | 1751 | notifications.error(i18n.get('steam-app-follow-failed')); 1752 | 1753 | return false; 1754 | } 1755 | 1756 | this._userFollowedApps.push(parseInt(appId)); 1757 | 1758 | return true; 1759 | }, 1760 | async unfollowApp(appId) { 1761 | log.debug(`steam.unfollowApp(${appId})`); 1762 | 1763 | const sessionId = await this.getSessionId(`https://store.steampowered.com/app/${appId}/`); 1764 | 1765 | if (!sessionId) { 1766 | notifications.error(i18n.get('steam-store-not-logged')); 1767 | 1768 | return false; 1769 | } 1770 | 1771 | log.debug(`steam.unfollowApp() : making request : ${this._followAppUrl}`); 1772 | 1773 | const response = await $GM.xmlHttpRequest({ 1774 | method: 'POST', 1775 | url: this._followAppUrl, 1776 | headers: { 1777 | 'Content-Type': 'application/x-www-form-urlencoded', 1778 | 'Origin': 'https://store.steampowered.com' 1779 | }, 1780 | data: $J.param({sessionid: sessionId, appid: appId, unfollow: '1'}) 1781 | }); 1782 | 1783 | if (response.status !== 200) { 1784 | log.debug(`steam.unfollowApp() : request failed : ${response.status}`); 1785 | 1786 | notifications.error(i18n.get('steam-app-unfollow-failed')); 1787 | 1788 | return false; 1789 | } 1790 | 1791 | if (response.responseText != 'true') { 1792 | log.debug('steam.unfollowApp() : not success'); 1793 | 1794 | notifications.error(i18n.get('steam-app-unfollow-failed')); 1795 | 1796 | return false; 1797 | } 1798 | 1799 | const index = this._userFollowedApps.indexOf(parseInt(appId)); 1800 | 1801 | if (index !== -1) { 1802 | this._userFollowedApps.splice(index, 1); 1803 | } 1804 | 1805 | return true; 1806 | }, 1807 | async addToLibrary(appId) { 1808 | log.debug(`steam.addToLibrary(${appId})`); 1809 | log.debug(`steam.addToLibrary() : making request : https://store.steampowered.com/app/${appId}/`); 1810 | 1811 | let response = await $GM.xmlHttpRequest({ 1812 | method: 'GET', 1813 | url: `https://store.steampowered.com/app/${appId}/` 1814 | }); 1815 | 1816 | if (response.status !== 200) { 1817 | log.debug(`steam.addToLibrary() : request failed : ${response.status}`); 1818 | 1819 | notifications.error(i18n.get('steam-app-add-to-library-failed')); 1820 | 1821 | return false; 1822 | } 1823 | 1824 | if (response.responseText.includes('game_area_already_owned')) { 1825 | log.debug('steam.addToLibrary() : already owned'); 1826 | 1827 | return true; 1828 | } 1829 | 1830 | const sessionId = response.responseText.match(/g_sessionID\s*=\s*"(.+?)"/); 1831 | 1832 | if (!sessionId) { 1833 | log.debug('steam.addToLibrary() : g_sessionID not found'); 1834 | 1835 | notifications.error(i18n.get('steam-app-add-to-library-failed')); 1836 | 1837 | return false; 1838 | } 1839 | 1840 | const subId = response.responseText.match(/AddFreeLicense\(\s*(\d+)/); 1841 | 1842 | if (!subId) { 1843 | log.debug('steam.addToLibrary() : subId not found'); 1844 | 1845 | notifications.error(i18n.get('steam-app-add-to-library-failed')); 1846 | 1847 | return false; 1848 | } 1849 | 1850 | log.debug(`steam.addToLibrary() : making request : https://store.steampowered.com/app/${subId[1]}/`); 1851 | 1852 | response = await $GM.xmlHttpRequest({ 1853 | method: 'POST', 1854 | url: this._addToLibraryUrl + subId[1], 1855 | headers: { 1856 | 'Content-Type': 'application/x-www-form-urlencoded', 1857 | 'Origin': 'https://store.steampowered.com' 1858 | }, 1859 | data: $J.param({sessionid: sessionId[1], ajax: 'true'}) 1860 | }); 1861 | 1862 | if (response.status !== 200) { 1863 | log.debug(`steam.addToLibrary() : request failed : ${response.status}`); 1864 | 1865 | notifications.error(i18n.get('steam-app-add-to-library-failed')); 1866 | 1867 | return false; 1868 | } 1869 | 1870 | this._userOwnedApps.push(parseInt(appId)); 1871 | 1872 | return true; 1873 | } 1874 | }; 1875 | 1876 | const buttons = { 1877 | count: 0, 1878 | _elements: {}, 1879 | _css: {}, 1880 | _steamKeys: [], 1881 | _steamGroups: [], 1882 | _steamAppWishlist: [], 1883 | _steamAppFollow: [], 1884 | _steamAppAddToLibrary: [], 1885 | _init() { 1886 | if (this._elements.main) { 1887 | return; 1888 | } 1889 | 1890 | this._css.mainId = utils.randomString(11); 1891 | this._css.wrapperId = utils.randomString(11); 1892 | this._css.buttonsId = utils.randomString(11); 1893 | this._css.buttonClass = utils.randomString(11); 1894 | this._css.buttonContentClass = utils.randomString(11); 1895 | this._css.disabledClass = utils.randomString(11); 1896 | this._css.spinnerClass = utils.randomString(11); 1897 | this._css.spinnerKeyframe = utils.randomString(11); 1898 | this._css.progressClass = utils.randomString(11); 1899 | this._css.moverId = utils.randomString(11); 1900 | this._css.resizerId = utils.randomString(11); 1901 | this._css.scrollUpId = utils.randomString(11); 1902 | this._css.scrollDownId = utils.randomString(11); 1903 | 1904 | $J('head').append( 1905 | `` 2075 | ); 2076 | 2077 | this._elements.main = $J( 2078 | `
2079 | 2080 |
2081 |
2082 | 2083 | 2084 |
2085 | 2086 |
` 2087 | ); 2088 | 2089 | this._elements.main.hide(); 2090 | this._elements.main.appendTo('body'); 2091 | 2092 | this._elements.wrapper = $J(`#${this._css.wrapperId}`); 2093 | this._elements.buttons = $J(`#${this._css.buttonsId}`); 2094 | this._elements.mover = $J(`#${this._css.moverId}`); 2095 | this._elements.resizer = $J(`#${this._css.resizerId}`); 2096 | this._elements.scrollUp = $J(`#${this._css.scrollUpId}`); 2097 | this._elements.scrollDown = $J(`#${this._css.scrollDownId}`); 2098 | 2099 | this.marginTop = parseInt(this._elements.main.css('margin-top')); 2100 | this.marginBottom = parseInt(this._elements.main.css('margin-bottom')); 2101 | 2102 | this.position(state.position); 2103 | this.maxHeight(state.maxHeight); 2104 | 2105 | const scroll = () => { 2106 | this._elements.buttons.animate({ 2107 | scrollTop: this._scrollAmount 2108 | }, 50, 'linear', () => { 2109 | if (this._scrollAmount !== '') { 2110 | scroll(); 2111 | } 2112 | }); 2113 | }; 2114 | 2115 | this._elements.scrollDown.on('mouseenter', () => { 2116 | if (this._dragging || this._resizing) { 2117 | return; 2118 | } 2119 | 2120 | this._scrollAmount = '+=10'; 2121 | scroll(); 2122 | }).on('mouseleave', () => { 2123 | this._scrollAmount = ''; 2124 | }); 2125 | 2126 | this._elements.scrollUp.on('mouseenter', () => { 2127 | if (this._dragging || this._resizing) { 2128 | return; 2129 | } 2130 | 2131 | this._scrollAmount = '-=10'; 2132 | scroll(); 2133 | }).on('mouseleave', () => { 2134 | this._scrollAmount = ''; 2135 | }); 2136 | 2137 | this._elements.mover.mousedown((e) => { 2138 | if (e.which !== 1) { 2139 | return; 2140 | } 2141 | 2142 | e.preventDefault(); 2143 | 2144 | this._dragging = true; 2145 | this._mouseYOld = e.clientY; 2146 | 2147 | $J(document).mouseup(async () => { 2148 | this._dragging = false; 2149 | 2150 | $J(document).off('mouseup'); 2151 | $J(document).off('mousemove'); 2152 | }); 2153 | 2154 | $J(document).mousemove(async (e) => { 2155 | e.preventDefault(); 2156 | 2157 | this.position(this.position() - (this._mouseYOld - e.clientY)); 2158 | this._mouseYOld = e.clientY; 2159 | }); 2160 | }); 2161 | 2162 | this._elements.resizer.mousedown((e) => { 2163 | if (e.which !== 1) { 2164 | return; 2165 | } 2166 | 2167 | e.preventDefault(); 2168 | 2169 | this._resizing = true; 2170 | this._mouseYOld = e.clientY; 2171 | 2172 | $J(document).mouseup(async () => { 2173 | this._resizing = false; 2174 | 2175 | $J(document).off('mouseup'); 2176 | $J(document).off('mousemove'); 2177 | }); 2178 | 2179 | $J(document).mousemove((e) => { 2180 | e.preventDefault(); 2181 | 2182 | const offset = this._mouseYOld - e.clientY; 2183 | const butonsHeight = Math.ceil(this._elements.buttons.height()); 2184 | 2185 | this._mouseYOld = e.clientY; 2186 | 2187 | if (offset < 0) { 2188 | const butonsInnerHeight = Math.ceil(this._elements.buttons.innerHeight()); 2189 | const mainOuterHeight = Math.ceil(this._elements.main.outerHeight(true)); 2190 | const butonsScrollHeight = Math.ceil(this._elements.buttons.prop('scrollHeight')); 2191 | 2192 | if (butonsInnerHeight >= butonsScrollHeight) { 2193 | return; 2194 | } 2195 | 2196 | if (this.position() > $J(window).height() - mainOuterHeight) { 2197 | return; 2198 | } 2199 | } else if (butonsHeight <= config.buttonsSize) { 2200 | return; 2201 | } 2202 | 2203 | const maxHeight = butonsHeight - offset < config.buttonsSize ? config.buttonsSize : butonsHeight - offset; 2204 | 2205 | if (parseInt(this.maxHeight()) === maxHeight) { 2206 | return; 2207 | } 2208 | 2209 | this.maxHeight(maxHeight); 2210 | 2211 | this._updateResizer(); 2212 | this._updateScroll(); 2213 | notifications.updatePosition(); 2214 | }); 2215 | }); 2216 | }, 2217 | async add(options) { 2218 | const self = this; 2219 | const button = { 2220 | clickable: true, 2221 | remove() { 2222 | log.debug('button.remove()'); 2223 | 2224 | switch (this.element.data('type')) { 2225 | case 'steam-key': { 2226 | const key = this.element.data('steamKey'); 2227 | 2228 | if (key) { 2229 | const idx = self._steamKeys.indexOf(key); 2230 | 2231 | if (idx !== -1) { 2232 | self._steamKeys.splice(idx, 1); 2233 | } 2234 | } 2235 | 2236 | break; 2237 | } 2238 | case 'steam-group': { 2239 | const group = this.element.data('steamGroup'); 2240 | 2241 | if (group) { 2242 | const idx = self._steamGroups.indexOf(group); 2243 | 2244 | if (idx !== -1) { 2245 | self._steamGroups.splice(idx, 1); 2246 | } 2247 | } 2248 | 2249 | break; 2250 | } 2251 | case 'steam-app-wishlist': { 2252 | const app = this.element.data('steamApp'); 2253 | 2254 | if (app) { 2255 | const idx = self._steamAppWishlist.indexOf(app); 2256 | 2257 | if (idx !== -1) { 2258 | self._steamAppWishlist.splice(idx, 1); 2259 | } 2260 | } 2261 | 2262 | break; 2263 | } 2264 | case 'steam-app-follow': { 2265 | const app = this.element.data('steamApp'); 2266 | 2267 | if (app) { 2268 | const idx = self._steamAppFollow.indexOf(app); 2269 | 2270 | if (idx !== -1) { 2271 | self._steamAppFollow.splice(idx, 1); 2272 | } 2273 | } 2274 | 2275 | break; 2276 | } 2277 | case 'steam-app-add': { 2278 | const app = this.element.data('steamApp'); 2279 | 2280 | if (app) { 2281 | const idx = self._steamAppAddToLibrary.indexOf(app); 2282 | 2283 | if (idx !== -1) { 2284 | self._steamAppAddToLibrary.splice(idx, 1); 2285 | } 2286 | } 2287 | 2288 | break; 2289 | } 2290 | } 2291 | 2292 | this.element.remove(); 2293 | self.count--; 2294 | self.visible(self.count > 0); 2295 | }, 2296 | color(color) { 2297 | log.debug(`button.color("${color}")`); 2298 | 2299 | this.element.css('background-color', color); 2300 | }, 2301 | enabled(value) { 2302 | log.debug(`button.enabled(${value})`); 2303 | 2304 | if (typeof value === 'undefined') { 2305 | return !this.element.hasClass(self._css.disabledClass); 2306 | } 2307 | 2308 | if (value) { 2309 | if (this.attr('data-working') || this.attr('data-not-enable') || !this.clickable) { 2310 | return; 2311 | } 2312 | 2313 | this.element.removeClass(self._css.disabledClass); 2314 | } else { 2315 | if (this.attr('data-cancellable') && !this.attr('data-cancelled')) { 2316 | return; 2317 | } 2318 | 2319 | this.element.addClass(self._css.disabledClass); 2320 | } 2321 | }, 2322 | content(content) { 2323 | log.debug(`button.content("${content}")`); 2324 | 2325 | if (typeof content === 'undefined') { 2326 | return this.element.children(`.${self._css.buttonContentClass}`).html(); 2327 | } 2328 | 2329 | this.element.children(`.${self._css.buttonContentClass}`).html(content); 2330 | }, 2331 | attr(attr, value) { 2332 | if (typeof value === 'undefined') { 2333 | return this.element.attr(attr); 2334 | } else if (value === '') { 2335 | this.element.removeAttr(attr); 2336 | return; 2337 | } 2338 | 2339 | this.element.attr(attr, value); 2340 | }, 2341 | title(value) { 2342 | log.debug(`button.title("${value}")`); 2343 | 2344 | if (typeof value === 'undefined') { 2345 | return this.attr('title'); 2346 | } 2347 | 2348 | this.attr('title', value); 2349 | }, 2350 | progress(max, value) { 2351 | let pr = this.element.children(`.${self._css.progressClass}`); 2352 | 2353 | if (!max) { 2354 | if (pr.length) { 2355 | log.debug(`button.progress() : remove`); 2356 | 2357 | pr.remove(); 2358 | } 2359 | return; 2360 | } 2361 | 2362 | if (!pr.length) { 2363 | pr = $J(``).appendTo(this.element); 2364 | } 2365 | 2366 | log.debug(`button.progress(${max}, ${value})`); 2367 | 2368 | pr.css('width', `${(100 / max * value).toFixed(1)}%`); 2369 | }, 2370 | spinner(show) { 2371 | log.debug(`button.spinner(${show})`); 2372 | 2373 | if (typeof show === 'undefined') { 2374 | return this.element.hasClass(self._css.spinnerClass); 2375 | } 2376 | 2377 | if (show) { 2378 | this.element.addClass(self._css.spinnerClass); 2379 | } else { 2380 | this.element.removeClass(self._css.spinnerClass); 2381 | } 2382 | }, 2383 | done(value) { 2384 | this.element.attr('data-done', value ? '1' : '0'); 2385 | } 2386 | }; 2387 | 2388 | if (options.type === 'info') { 2389 | let noti; 2390 | 2391 | options.content = icons.info2; 2392 | options.title = i18n.get('useful-info'); 2393 | options.sticky = true; 2394 | options.click = () => { 2395 | if (noti && !noti.removed) { 2396 | noti.remove(); 2397 | return; 2398 | } 2399 | 2400 | noti = notifications.info(i18n.format(typeof options.info !== 'function' ? options.info : options.info()), {timeout: 0}); 2401 | }; 2402 | } else if (options.type === 'steam-key') { 2403 | if (typeof options.steamKey !== 'string' || this.isSteamKeyAdded(options.steamKey)) { 2404 | return false; 2405 | } 2406 | 2407 | log.debug(`buttons.add() : steam-key button : ${options.steamKey}`); 2408 | 2409 | this._steamKeys.push(options.steamKey); 2410 | 2411 | options.title = i18n.get('steam-activate-key', {key: options.steamKey}); 2412 | options.content = icons.key; 2413 | options.prepend = true; 2414 | options.insertByType = true; 2415 | options.data = {steamKey: options.steamKey}; 2416 | options.click = (p) => { 2417 | steam.openKeyActivationPage(p.button.element.data('steamKey')); 2418 | }; 2419 | } else if (options.type === 'steam-group') { 2420 | if (!config.steamGroups || !options.steamGroup) { 2421 | return false; 2422 | } 2423 | if (this.isSteamGroupAdded(options.steamGroup)) { 2424 | return false; 2425 | } 2426 | if (!await steam.init()) { 2427 | return false; 2428 | } 2429 | 2430 | log.debug(`buttons.add() : steam-group button : ${options.steamGroup}`); 2431 | 2432 | let group = options.steamGroup; 2433 | 2434 | if (group.includes('/')) { 2435 | let ext = steam.extractGroupName(group); 2436 | 2437 | if (!ext) { 2438 | const res = await utils.resolveUrl(group); 2439 | 2440 | if (res === false) { 2441 | return false; 2442 | } 2443 | 2444 | group = res; 2445 | ext = steam.extractGroupName(group); 2446 | 2447 | if (!ext) { 2448 | log.debug(`buttons.add() : cannot extract group name : ${group}`); 2449 | 2450 | return false; 2451 | } 2452 | } 2453 | 2454 | group = ext; 2455 | 2456 | if (this.isSteamGroupAdded(group)) { 2457 | return false; 2458 | } 2459 | 2460 | this._steamGroups.push(options.steamGroup); 2461 | } 2462 | 2463 | group = group.toLowerCase(); 2464 | 2465 | this._steamGroups.push(group); 2466 | 2467 | options.data = {steamGroup: group}; 2468 | 2469 | if (!steam.isJoinedGroup(group)) { 2470 | options.done = false; 2471 | options.title = i18n.get('steam-group-join', {group: group}); 2472 | } else { 2473 | options.done = true; 2474 | options.title = i18n.get('steam-group-leave', {group: group}); 2475 | } 2476 | 2477 | options.content = icons.group; 2478 | options.click = (p) => { 2479 | const group = p.button.element.data('steamGroup'); 2480 | 2481 | if (p.event.ctrlKey) { 2482 | steam.openGroupPage(group); 2483 | return; 2484 | } 2485 | 2486 | return new Promise(async (resolve) => { 2487 | if (!steam.isJoinedGroup(group)) { 2488 | if (await steam.joinGroup(group)) { 2489 | button.done(true); 2490 | button.title(i18n.get('steam-group-leave', {group: group})); 2491 | } 2492 | } else { 2493 | if (await steam.leaveGroup(group)) { 2494 | button.done(false); 2495 | button.title(i18n.get('steam-group-join', {group: group})); 2496 | } 2497 | } 2498 | 2499 | resolve(); 2500 | }); 2501 | }; 2502 | } else if (options.type === 'steam-app-wishlist') { 2503 | if (!config.steamAppWishlist || 2504 | !options.steamApp || 2505 | this.isSteamAppWishlistAdded(options.steamApp) || 2506 | steam.isAppOwned(options.steamApp) 2507 | ) { 2508 | return false; 2509 | } 2510 | if (!await steam.init()) { 2511 | return false; 2512 | } 2513 | 2514 | log.debug(`buttons.add() : steam-app-wishlist button : ${options.steamApp}`); 2515 | 2516 | let steamApp = options.steamApp; 2517 | 2518 | if (steamApp.includes('/')) { 2519 | let ext = steam.extractAppId(steamApp); 2520 | 2521 | if (!ext) { 2522 | const res = await utils.resolveUrl(steamApp); 2523 | 2524 | if (res === false) { 2525 | return false; 2526 | } 2527 | 2528 | steamApp = res; 2529 | ext = steam.extractAppId(steamApp); 2530 | 2531 | if (!ext) { 2532 | log.debug(`buttons.add() : cannot extract app id : ${steamApp}`); 2533 | 2534 | return false; 2535 | } 2536 | } 2537 | 2538 | steamApp = ext; 2539 | 2540 | if (this.isSteamAppWishlistAdded(steamApp)) { 2541 | return false; 2542 | } 2543 | 2544 | this._steamAppWishlist.push(options.steamApp); 2545 | } 2546 | 2547 | if (steam.isAppOwned(steamApp)) { 2548 | return false; 2549 | } 2550 | 2551 | this._steamAppWishlist.push(steamApp); 2552 | 2553 | options.data = {steamApp: steamApp}; 2554 | 2555 | if (!steam.isAppWishlisted(steamApp)) { 2556 | options.done = false; 2557 | options.title = i18n.get('steam-app-wishlist-add', {appId: steamApp}); 2558 | } else { 2559 | options.done = true; 2560 | options.title = i18n.get('steam-app-wishlist-remove', {appId: steamApp}); 2561 | } 2562 | 2563 | options.content = icons.wishlist; 2564 | options.click = (p) => { 2565 | const appId = p.button.element.data('steamApp'); 2566 | 2567 | if (p.event.ctrlKey) { 2568 | steam.openAppPage(appId); 2569 | return; 2570 | } 2571 | 2572 | return new Promise(async (resolve) => { 2573 | if (!steam.isAppWishlisted(appId)) { 2574 | if (await steam.addToWishlist(appId)) { 2575 | button.done(true); 2576 | button.title(i18n.get('steam-app-wishlist-remove', {appId: appId})); 2577 | } 2578 | } else { 2579 | if (await steam.removeFromWishlist(appId)) { 2580 | button.done(false); 2581 | button.title(i18n.get('steam-app-wishlist-add', {appId: appId})); 2582 | } 2583 | } 2584 | 2585 | resolve(); 2586 | }); 2587 | }; 2588 | } else if (options.type === 'steam-app-follow') { 2589 | if (!config.steamAppWishlist || !options.steamApp) { 2590 | return false; 2591 | } 2592 | if (this.isSteamAppFollowAdded(options.steamApp)) { 2593 | return false; 2594 | } 2595 | if (!await steam.init()) { 2596 | return false; 2597 | } 2598 | 2599 | log.debug(`buttons.add() : steam-app-follow button : ${options.steamApp}`); 2600 | 2601 | let steamApp = options.steamApp; 2602 | 2603 | if (steamApp.includes('/')) { 2604 | let ext = steam.extractAppId(steamApp); 2605 | 2606 | if (!ext) { 2607 | const res = await utils.resolveUrl(steamApp); 2608 | 2609 | if (res === false) { 2610 | return false; 2611 | } 2612 | 2613 | steamApp = res; 2614 | ext = steam.extractAppId(steamApp); 2615 | 2616 | if (!ext) { 2617 | log.debug(`buttons.add() : cannot extract app id : ${steamApp}`); 2618 | 2619 | return false; 2620 | } 2621 | } 2622 | 2623 | steamApp = ext; 2624 | 2625 | if (this.isSteamAppFollowAdded(steamApp)) { 2626 | return false; 2627 | } 2628 | 2629 | this._steamAppFollow.push(options.steamApp); 2630 | } 2631 | 2632 | this._steamAppFollow.push(steamApp); 2633 | 2634 | options.data = {steamApp: steamApp}; 2635 | 2636 | if (!steam.isAppFollowed(steamApp)) { 2637 | options.done = false; 2638 | options.title = i18n.get('steam-app-follow', {appId: steamApp}); 2639 | } else { 2640 | options.done = true; 2641 | options.title = i18n.get('steam-app-unfollow', {appId: steamApp}); 2642 | } 2643 | 2644 | options.content = icons.follow; 2645 | options.click = (p) => { 2646 | const appId = p.button.element.data('steamApp'); 2647 | 2648 | if (p.event.ctrlKey) { 2649 | steam.openAppPage(appId); 2650 | return; 2651 | } 2652 | 2653 | return new Promise(async (resolve) => { 2654 | if (!steam.isAppFollowed(appId)) { 2655 | if (await steam.followApp(appId)) { 2656 | button.done(true); 2657 | button.title(i18n.get('steam-app-follow', {appId: appId})); 2658 | } 2659 | } else { 2660 | if (await steam.unfollowApp(appId)) { 2661 | button.done(false); 2662 | button.title(i18n.get('steam-app-unfollow', {appId: appId})); 2663 | } 2664 | } 2665 | 2666 | resolve(); 2667 | }); 2668 | }; 2669 | } else if (options.type === 'steam-app-add') { 2670 | if (!config.steamAppAddToLibrary || !options.steamApp) { 2671 | return false; 2672 | } 2673 | if (this.isSteamAppAddToLibraryAdded(options.steamApp)) { 2674 | return false; 2675 | } 2676 | if (!await steam.init()) { 2677 | return false; 2678 | } 2679 | 2680 | log.debug(`buttons.add() : steam-app-add button : ${options.steamApp}`); 2681 | 2682 | let steamApp = options.steamApp; 2683 | 2684 | if (steamApp.includes('/')) { 2685 | let ext = steam.extractAppId(steamApp); 2686 | 2687 | if (!ext) { 2688 | const res = await utils.resolveUrl(steamApp); 2689 | 2690 | if (res === false) { 2691 | return false; 2692 | } 2693 | 2694 | steamApp = res; 2695 | ext = steam.extractAppId(steamApp); 2696 | 2697 | if (!ext) { 2698 | log.debug(`buttons.add() : cannot extract app id : ${steamApp}`); 2699 | 2700 | return false; 2701 | } 2702 | } 2703 | 2704 | steamApp = ext; 2705 | 2706 | if (this.isSteamAppAddToLibraryAdded(steamApp)) { 2707 | return false; 2708 | } 2709 | 2710 | this._steamAppAddToLibrary.push(options.steamApp); 2711 | } 2712 | 2713 | this._steamAppAddToLibrary.push(steamApp); 2714 | 2715 | options.data = {steamApp: steamApp}; 2716 | 2717 | if (steam.isAppOwned(steamApp)) { 2718 | return false; 2719 | } 2720 | 2721 | options.done = false; 2722 | options.title = i18n.get('steam-app-add-to-library', {appId: steamApp}); 2723 | options.content = icons.library; 2724 | options.click = (p) => { 2725 | const appId = p.button.element.data('steamApp'); 2726 | 2727 | if (p.event.ctrlKey) { 2728 | steam.openAppPage(appId); 2729 | return; 2730 | } 2731 | 2732 | if (!steam.isAppOwned(steamApp)) { 2733 | return new Promise(async (resolve) => { 2734 | if (await steam.addToLibrary(appId)) { 2735 | button.clickable = false; 2736 | button.done(true); 2737 | } 2738 | 2739 | resolve(); 2740 | }); 2741 | } 2742 | }; 2743 | } else if (options.type === 'tasks') { 2744 | log.debug('buttons.add() : tasks button'); 2745 | 2746 | options.title = i18n.get('confirm-tasks'); 2747 | options.content = icons.checkmark; 2748 | options.sticky = true; 2749 | options.overlay = true; 2750 | options.disable = true; 2751 | } 2752 | 2753 | if (typeof options.click !== 'function') { 2754 | return false; 2755 | } 2756 | 2757 | this._init(); 2758 | 2759 | const element = $J( 2760 | ` 2761 | 2762 | ` 2763 | ); 2764 | 2765 | if (typeof options.title !== 'undefined') { 2766 | element.attr('title', options.title); 2767 | } 2768 | if (typeof options.color === 'string') { 2769 | element.css('background-color', options.color); 2770 | } 2771 | if (typeof options.style === 'string') { 2772 | element.attr('style', options.style); 2773 | } 2774 | if (typeof options.data === 'object') { 2775 | element.data(options.data); 2776 | } 2777 | if (typeof options.content !== 'undefined') { 2778 | element.children(`.${this._css.buttonContentClass}`).html(options.content); 2779 | } 2780 | if (typeof options.attr === 'object') { 2781 | Object.keys(options.attr).forEach((key) => { 2782 | element.attr(key, options.attr[key]); 2783 | }); 2784 | } 2785 | if (typeof options.done === 'boolean') { 2786 | element.attr('data-done', options.done ? '1' : '0'); 2787 | } 2788 | 2789 | element.attr('data-type', options.type); 2790 | element.data('button', button); 2791 | 2792 | if (options.sticky) { 2793 | element.attr('data-sticky', '1'); 2794 | element.prependTo(this._elements.buttons); 2795 | } else { 2796 | if (typeof options.done === 'boolean') { 2797 | if (options.done) { 2798 | element.appendTo(this._elements.buttons); 2799 | } else { 2800 | const btns = this._elements.buttons.children(`[data-done="1"]`); 2801 | 2802 | if (btns.length) { 2803 | element.insertBefore(btns.first()); 2804 | } else { 2805 | element.appendTo(this._elements.buttons); 2806 | } 2807 | } 2808 | } else { 2809 | if (options.insertByType) { 2810 | let btns = this._elements.buttons.children(`[data-type="${options.type}"]`); 2811 | 2812 | if (btns.length) { 2813 | if (options.prependByType) { 2814 | element.insertBefore(btns.first()); 2815 | } else { 2816 | element.insertAfter(btns.last()); 2817 | } 2818 | } else { 2819 | if (options.prepend) { 2820 | btns = this._elements.buttons.children('[data-sticky="1"]'); 2821 | 2822 | if (btns.length) { 2823 | element.insertAfter(btns.last()); 2824 | } else { 2825 | element.prependTo(this._elements.buttons); 2826 | } 2827 | } else { 2828 | element.appendTo(this._elements.buttons); 2829 | } 2830 | } 2831 | } else { 2832 | if (options.prepend) { 2833 | const btns = this._elements.buttons.children(':not([data-sticky="1"])'); 2834 | 2835 | if (btns.length) { 2836 | element.insertBefore(btns.first()); 2837 | } else { 2838 | element.prependTo(this._elements.buttons); 2839 | } 2840 | } else { 2841 | element.appendTo(this._elements.buttons); 2842 | } 2843 | } 2844 | } 2845 | } 2846 | 2847 | button.element = element; 2848 | 2849 | const args = { 2850 | button: button, 2851 | self: options._self, 2852 | site: state.site, 2853 | matchedSelector: options._matchedSelector, 2854 | matchedElement: options._matchedElement 2855 | }; 2856 | 2857 | element.on('click', async (e) => { 2858 | e.preventDefault(); 2859 | 2860 | if (!button.enabled() || !button.clickable) { 2861 | return; 2862 | } 2863 | 2864 | if (options.cancellable && args._working) { 2865 | log.debug('cancelling click()...'); 2866 | 2867 | button.attr('data-cancelled', '1'); 2868 | button.enabled(false); 2869 | 2870 | args.cancelled = true; 2871 | 2872 | return; 2873 | } 2874 | 2875 | args.event = e; 2876 | 2877 | log.debug('calling click()...'); 2878 | 2879 | let promise = false; 2880 | 2881 | try { 2882 | const res = options.click(args); 2883 | 2884 | if (res instanceof Promise) { 2885 | promise = true; 2886 | 2887 | button.attr('data-working', '1'); 2888 | 2889 | if (options._button) { 2890 | options._button._working = true; 2891 | } 2892 | if (options.overlay) { 2893 | overlay.visible(true); 2894 | } 2895 | if (!options.cancellable) { 2896 | button.enabled(false); 2897 | button.spinner(true); 2898 | } else { 2899 | args._working = true; 2900 | args._title = button.title(); 2901 | args._content = button.content(); 2902 | 2903 | button.title(i18n.get('cancel')); 2904 | button.content(icons.cancel); 2905 | button.attr('data-cancellable', '1'); 2906 | } 2907 | if (options.disable) { 2908 | self.enabled(false); 2909 | } 2910 | 2911 | await res; 2912 | } 2913 | } catch (e) { 2914 | log.error('click() exception :', e); 2915 | } finally { 2916 | if (promise) { 2917 | button.attr('data-working', ''); 2918 | 2919 | if (options._button) { 2920 | options._button._working = false; 2921 | } 2922 | if (options.overlay) { 2923 | overlay.visible(false); 2924 | } 2925 | if (!options.cancellable) { 2926 | button.spinner(false); 2927 | } else { 2928 | button.attr('data-cancelled', ''); 2929 | button.attr('data-cancellable', ''); 2930 | button.content(args._content); 2931 | 2932 | if (typeof args._title !== 'undefined') { 2933 | button.title(args._title); 2934 | } else { 2935 | button.title(''); 2936 | } 2937 | 2938 | args._working = false; 2939 | args.cancelled = false; 2940 | } 2941 | if (options.disable) { 2942 | self.enabled(true); 2943 | } 2944 | 2945 | button.enabled(true); 2946 | button.progress(); 2947 | } 2948 | 2949 | log.debug('click() done'); 2950 | } 2951 | }); 2952 | 2953 | log.debug('buttons.add() : added'); 2954 | 2955 | this.count++; 2956 | this.visible(true); 2957 | 2958 | return button; 2959 | }, 2960 | _updateHeightAndPosition() { 2961 | const mainTop = this.position(); 2962 | const mainOuterHeight = this.outerHeight(); 2963 | const buttonsHeight = Math.ceil(this._elements.buttons.height()); 2964 | const winHeight = $J(window).height(); 2965 | const heightDiff = mainOuterHeight - buttonsHeight; 2966 | 2967 | if (mainTop > winHeight - mainOuterHeight - this.marginTop - this.marginBottom) { 2968 | if (buttonsHeight > config.buttonsSize) { 2969 | this.maxHeight(winHeight - mainTop - this.marginTop - this.marginBottom - heightDiff); 2970 | } else { 2971 | if (mainTop > this.marginTop) { 2972 | this.position(winHeight - mainOuterHeight - this.marginTop - this.marginBottom); 2973 | } 2974 | } 2975 | } 2976 | }, 2977 | _updateResizer() { 2978 | if (Math.ceil(this._elements.buttons.height()) <= config.buttonsSize 2979 | && Math.ceil(this._elements.buttons.innerHeight()) >= Math.ceil(this._elements.buttons.prop('scrollHeight')) 2980 | ) { 2981 | this._elements.resizer.hide(); 2982 | } else { 2983 | this._elements.resizer.show(); 2984 | } 2985 | }, 2986 | _updateScroll() { 2987 | const scrollTop = Math.ceil(this._elements.buttons.scrollTop()); 2988 | 2989 | if (scrollTop + Math.ceil(this._elements.buttons.innerHeight()) >= Math.ceil(this._elements.buttons.prop('scrollHeight'))) { 2990 | this._elements.scrollDown.hide(); 2991 | } else { 2992 | this._elements.scrollDown.show(); 2993 | } 2994 | 2995 | if (scrollTop <= 0) { 2996 | this._elements.scrollUp.hide(); 2997 | } else { 2998 | this._elements.scrollUp.show(); 2999 | } 3000 | }, 3001 | visible(value) { 3002 | if (!this._elements.main) { 3003 | return false; 3004 | } 3005 | 3006 | if (typeof value === 'undefined') { 3007 | return this._elements.main.is(':visible'); 3008 | } 3009 | 3010 | log.debug(`buttons.visible(${value})`); 3011 | 3012 | const updateAll = () => { 3013 | this._updateResizer(); 3014 | this._updateScroll(); 3015 | this._updateHeightAndPosition(); 3016 | notifications.updatePosition(); 3017 | }; 3018 | 3019 | const resizeHandler = () => { 3020 | if ($J(window).height() === 0) { 3021 | return; 3022 | } 3023 | 3024 | updateAll(); 3025 | }; 3026 | 3027 | if (value) { 3028 | if (this._elements.main.is(':visible') !== value) { 3029 | $J(window).on('resize', resizeHandler); 3030 | 3031 | this._elements.buttons.on('scroll', () => { 3032 | this._updateScroll(); 3033 | }); 3034 | 3035 | this._elements.main.fadeIn(400, 'swing'); 3036 | } 3037 | 3038 | updateAll(); 3039 | } else { 3040 | $J(window).off('resize', resizeHandler); 3041 | this._elements.buttons.off('scroll'); 3042 | this._elements.main.hide(); 3043 | } 3044 | }, 3045 | position(top) { 3046 | const current = parseInt(this._elements.main.css('top')); 3047 | 3048 | if (isNaN(top)) { 3049 | return current; 3050 | } 3051 | 3052 | const max = $J(window).height() - Math.ceil(this._elements.main.outerHeight(true)); 3053 | 3054 | if (top < 0) { 3055 | top = 0; 3056 | } 3057 | if (top > max) { 3058 | top = max; 3059 | } 3060 | if (top === current) { 3061 | return; 3062 | } 3063 | 3064 | this._elements.main.css('top', `${top}px`); 3065 | $GM.setValue('position', top); 3066 | 3067 | this._updateScroll(); 3068 | notifications.updatePosition(); 3069 | }, 3070 | maxHeight(height) { 3071 | if (isNaN(height)) { 3072 | return parseInt(this._elements.buttons.css('max-height')); 3073 | } 3074 | 3075 | this._elements.buttons.css('max-height', `${height}px`); 3076 | $GM.setValue('maxHeight', height); 3077 | }, 3078 | outerHeight(includeMargin = false) { 3079 | if (!this._elements.main) { 3080 | return false; 3081 | } 3082 | 3083 | return Math.ceil(this._elements.main.outerHeight(includeMargin)); 3084 | }, 3085 | outerWidth(includeMargin = false) { 3086 | if (!this._elements.main) { 3087 | return false; 3088 | } 3089 | 3090 | return Math.ceil(this._elements.main.outerWidth(includeMargin)); 3091 | }, 3092 | enabled(value) { 3093 | if (!this._elements.buttons) { 3094 | return; 3095 | } 3096 | 3097 | log.debug(`buttons.enabled(${value})`); 3098 | 3099 | this._elements.buttons.children(`.${this._css.buttonClass}`).each((i, el) => { 3100 | const jel = $J(el); 3101 | 3102 | if (value) { 3103 | jel.removeAttr('data-not-enable'); 3104 | } else { 3105 | jel.attr('data-not-enable', '1'); 3106 | } 3107 | 3108 | jel.data('button').enabled(value); 3109 | }); 3110 | }, 3111 | isSteamKeyAdded(key) { 3112 | return this._steamKeys.indexOf(key) !== -1; 3113 | }, 3114 | isSteamGroupAdded(group) { 3115 | if (!group.includes('/')) { 3116 | group = group.toLowerCase(); 3117 | } 3118 | return this._steamGroups.indexOf(group) !== -1; 3119 | }, 3120 | isSteamAppWishlistAdded(id) { 3121 | return this._steamAppWishlist.indexOf(id) !== -1; 3122 | }, 3123 | isSteamAppFollowAdded(id) { 3124 | return this._steamAppFollow.indexOf(id) !== -1; 3125 | }, 3126 | isSteamAppAddToLibraryAdded(id) { 3127 | return this._steamAppAddToLibrary.indexOf(id) !== -1; 3128 | } 3129 | }; 3130 | 3131 | const notifications = { 3132 | type: { 3133 | success: 'success', 3134 | info: 'info', 3135 | warning: 'warning', 3136 | error: 'error' 3137 | }, 3138 | _css: {}, 3139 | _init() { 3140 | if (this._container) { 3141 | return; 3142 | } 3143 | 3144 | this._css.containerId = utils.randomString(11); 3145 | this._css.notificationClass = utils.randomString(11); 3146 | this._css.notificationIconClass = utils.randomString(11); 3147 | this._css.notificationContentClass = utils.randomString(11); 3148 | this._css.notificationTitleClass = utils.randomString(11); 3149 | this._css.notificationMessageClass = utils.randomString(11); 3150 | this._css.notificationClickableClass = utils.randomString(11); 3151 | this._css.notificationSuccessClass = utils.randomString(11); 3152 | this._css.notificationInfoClass = utils.randomString(11); 3153 | this._css.notificationWarningClass = utils.randomString(11); 3154 | this._css.notificationErrorClass = utils.randomString(11); 3155 | this._css.notificationCloseClass = utils.randomString(11); 3156 | 3157 | $J('head').append( 3158 | `` 3282 | ); 3283 | 3284 | this._container = $J(`
`).appendTo('body'); 3285 | }, 3286 | info(message, options) { 3287 | if (!options) { 3288 | options = {}; 3289 | } 3290 | 3291 | return this.add(Object.assign({}, options, {type: this.type.info, message: message})); 3292 | }, 3293 | success(message, options) { 3294 | if (!options) { 3295 | options = {}; 3296 | } 3297 | 3298 | return this.add(Object.assign({}, options, {type: this.type.success, message: message})); 3299 | }, 3300 | warning(message, options) { 3301 | if (!options) { 3302 | options = {}; 3303 | } 3304 | 3305 | return this.add(Object.assign({}, options, {type: this.type.warning, message: message})); 3306 | }, 3307 | error(message, options) { 3308 | if (!options) { 3309 | options = {}; 3310 | } 3311 | 3312 | return this.add(Object.assign({}, options, {type: this.type.error, message: message})); 3313 | }, 3314 | add(options) { 3315 | if (typeof options !== 'object' || typeof options.message === 'undefined') { 3316 | return; 3317 | } 3318 | 3319 | log.debug('notifications.add()'); 3320 | 3321 | const self = this; 3322 | 3323 | const noti = { 3324 | _timer: null, 3325 | element: null, 3326 | removed: false, 3327 | remove(timeout) { 3328 | log.debug(`notification.remove(${timeout})`); 3329 | 3330 | if (timeout) { 3331 | this._timer = setTimeout(() => { 3332 | this._timer = null; 3333 | this._timedOut = true; 3334 | if (!this._hover) { 3335 | this.remove(); 3336 | } 3337 | }, timeout); 3338 | return; 3339 | } else if (this._timer) { 3340 | clearTimeout(this._timer); 3341 | } 3342 | 3343 | this.removed = true; 3344 | this.element.fadeOut(400, 'swing', () => { 3345 | this.element.remove(); 3346 | self.updatePosition(); 3347 | }); 3348 | }, 3349 | message(html) { 3350 | log.debug(`notification.message("${html}")`); 3351 | 3352 | const el = this.element.find(`.${self._css.notificationMessageClass}`); 3353 | 3354 | if (typeof html === 'undefined') { 3355 | return el.html(); 3356 | } 3357 | 3358 | el.html(html); 3359 | } 3360 | }; 3361 | 3362 | this._init(); 3363 | 3364 | noti.element = $J( 3365 | `
3366 | 3367 |
3368 |
3369 | Giveaway Companion 3370 | ${icons.close} 3371 |
3372 |
${options.message}
3373 |
3374 |
` 3375 | ); 3376 | 3377 | noti.element.hide(); 3378 | 3379 | noti.element.data('notification', noti); 3380 | 3381 | switch (options.type) { 3382 | case this.type.success: 3383 | noti.element.addClass(this._css.notificationSuccessClass); 3384 | noti.element.children(`.${this._css.notificationIconClass}`).html(icons.success); 3385 | break; 3386 | case this.type.warning: 3387 | noti.element.addClass(this._css.notificationWarningClass); 3388 | noti.element.children(`.${this._css.notificationIconClass}`).html(icons.warning); 3389 | break; 3390 | case this.type.error: 3391 | noti.element.addClass(this._css.notificationErrorClass); 3392 | noti.element.children(`.${this._css.notificationIconClass}`).html(icons.error); 3393 | break; 3394 | default: 3395 | noti.element.addClass(this._css.notificationInfoClass); 3396 | noti.element.children(`.${this._css.notificationIconClass}`).html(icons.info); 3397 | } 3398 | 3399 | if (typeof options.click === 'function') { 3400 | noti.element.on('click', (e) => { 3401 | log.debug('calling notification.click()...'); 3402 | 3403 | try { 3404 | options.click({ 3405 | event: e, 3406 | notification: noti 3407 | }); 3408 | } catch (e) { 3409 | log.error('notification.click() exception :', e); 3410 | } finally { 3411 | log.debug('notification.click() done'); 3412 | } 3413 | }); 3414 | 3415 | noti.element.addClass(this._css.notificationClickableClass); 3416 | } else { 3417 | const con = typeof options.closeOnClick !== 'undefined' ? options.closeOnClick : config.notifications.closeOnClick; 3418 | 3419 | if (con) { 3420 | noti.element.on('click', (e) => { 3421 | if (e.target.nodeName === 'A') { 3422 | return; 3423 | } 3424 | 3425 | noti.remove(); 3426 | }); 3427 | noti.element.addClass(this._css.notificationClickableClass); 3428 | } 3429 | } 3430 | 3431 | noti.element.on('mouseenter', () => { 3432 | noti._hover = true; 3433 | }).on('mouseleave', () => { 3434 | noti._hover = false; 3435 | 3436 | if (noti._timedOut) { 3437 | noti.remove(config.notifications.extendedTimeout); 3438 | } 3439 | }); 3440 | 3441 | noti.element.find(`.${this._css.notificationCloseClass}`).on('click', () => { 3442 | noti.remove(); 3443 | }); 3444 | 3445 | const nots = this.getAll(); 3446 | 3447 | if (nots.length >= config.notifications.maxCount) { 3448 | if (config.notifications.newestOnTop) { 3449 | nots[nots.length - 1].remove(); 3450 | } else { 3451 | nots[0].remove(); 3452 | } 3453 | } 3454 | 3455 | if (config.notifications.newestOnTop) { 3456 | this._container.prepend(noti.element); 3457 | } else { 3458 | this._container.append(noti.element); 3459 | } 3460 | 3461 | const timeout = typeof options.timeout === 'number' ? options.timeout : config.notifications.timeout; 3462 | 3463 | noti.element.fadeIn(400, 'swing', () => { 3464 | if (timeout > 0) { 3465 | noti.remove(timeout); 3466 | } 3467 | }); 3468 | 3469 | this.updatePosition(); 3470 | 3471 | return noti; 3472 | }, 3473 | clear() { 3474 | log.debug('notifications.clear()'); 3475 | 3476 | const nots = this.getAll(); 3477 | 3478 | for (const not of nots) { 3479 | not.remove(); 3480 | } 3481 | }, 3482 | getAll() { 3483 | if (!this._container) { 3484 | return []; 3485 | } 3486 | 3487 | const nots = []; 3488 | 3489 | this._container.children().each((i, el) => { 3490 | const obj = $J(el).data('notification'); 3491 | 3492 | if (obj && !obj.removed) { 3493 | nots.push(obj); 3494 | } 3495 | }); 3496 | 3497 | return nots; 3498 | }, 3499 | updatePosition() { 3500 | if (!this._container || !buttons.visible()) { 3501 | return; 3502 | } 3503 | 3504 | if (buttons.position() + buttons.outerHeight(true) > $J(window).height() - Math.ceil(this._container.outerHeight(true))) { 3505 | this._container.css('right', `${buttons.outerWidth()}px`); 3506 | } else { 3507 | this._container.css('right', 0); 3508 | } 3509 | } 3510 | }; 3511 | 3512 | const log = { 3513 | debug(...args) { 3514 | if (config.debug) { 3515 | this._console('log', args); 3516 | } 3517 | }, 3518 | info(...args) { 3519 | this._console('log', args); 3520 | }, 3521 | warn(...args) { 3522 | this._console('warn', args); 3523 | }, 3524 | error(...args) { 3525 | this._console('error', args); 3526 | }, 3527 | _console(method, args) { 3528 | const ar = Array.prototype.slice.call(args, 0); 3529 | ar.unshift('GC ::'); 3530 | console[method](...ar); 3531 | } 3532 | }; 3533 | 3534 | const $J = jQuery.noConflict(true); 3535 | const $GM = { 3536 | xmlHttpRequest(details) { 3537 | return new Promise((resolve) => { 3538 | details.timeout = 30000; 3539 | details.onload = resolve; 3540 | details.onerror = resolve; 3541 | details.ontimeout = resolve; 3542 | details.onabort = resolve; 3543 | 3544 | const func = typeof GM !== 'undefined' ? GM.xmlHttpRequest : GM_xmlhttpRequest; 3545 | func(details); 3546 | }); 3547 | }, 3548 | setValue: typeof GM !== 'undefined' ? GM.setValue : GM_setValue, 3549 | getValue(...args) { 3550 | if (typeof GM !== 'undefined') { 3551 | return GM.getValue(...args); 3552 | } 3553 | 3554 | return new Promise((resolve, reject) => { 3555 | try { 3556 | resolve(GM_getValue(...args)); 3557 | } catch (e) { 3558 | reject(e); 3559 | } 3560 | }); 3561 | } 3562 | }; 3563 | 3564 | (async () => { 3565 | const checkStringCondition = (variable, compare) => { 3566 | if (typeof variable === 'string') { 3567 | if (variable === compare) { 3568 | return true; 3569 | } 3570 | } else if (variable instanceof RegExp) { 3571 | if (variable.test(compare)) { 3572 | return true; 3573 | } 3574 | } else if (Array.isArray(variable)) { 3575 | for (const v of variable) { 3576 | if (checkStringCondition(v, compare)) { 3577 | return true; 3578 | } 3579 | } 3580 | } 3581 | 3582 | return false; 3583 | }; 3584 | 3585 | const checkSite = async () => { 3586 | const checkConditions = (obj, data) => { 3587 | const checkElementCondition = (selector, setData) => { 3588 | let res; 3589 | 3590 | if (selector.startsWith('!')) { 3591 | res = $J(selector.substring(1)).length === 0; 3592 | } else { 3593 | res = $J(selector); 3594 | 3595 | if (res.length && setData && typeof data === 'object') { 3596 | data.matchedSelector = selector; 3597 | data.matchedElement = res; 3598 | } 3599 | 3600 | res = res.length > 0; 3601 | } 3602 | 3603 | return res; 3604 | }; 3605 | 3606 | if (typeof obj.check === 'function') { 3607 | if (!obj.check({ 3608 | self: obj, 3609 | site: state.site 3610 | })) { 3611 | return false; 3612 | } 3613 | } 3614 | 3615 | if (obj !== state.site) { 3616 | if (typeof obj.host === 'string') { 3617 | if (state.host !== obj.host) { 3618 | return false; 3619 | } 3620 | } else if (obj.host instanceof RegExp) { 3621 | if (!obj.host.test(state.host)) { 3622 | return false; 3623 | } 3624 | } else if (Array.isArray(obj.host)) { 3625 | let match = false; 3626 | 3627 | for (const host of obj.host) { 3628 | if (state.host === host) { 3629 | match = true; 3630 | break; 3631 | } 3632 | } 3633 | 3634 | if (!match) { 3635 | return false; 3636 | } 3637 | } 3638 | } 3639 | 3640 | if (typeof obj.href !== 'undefined' && !checkStringCondition(obj.href, window.location.href)) { 3641 | return false; 3642 | } 3643 | 3644 | if (typeof obj.path !== 'undefined' && !checkStringCondition(obj.path, window.location.pathname)) { 3645 | return false; 3646 | } 3647 | 3648 | if (typeof obj.element === 'string' && !checkElementCondition(obj.element, true)) { 3649 | return false; 3650 | } 3651 | 3652 | if (Array.isArray(obj.elementOr)) { 3653 | let match = false; 3654 | 3655 | for (const e of obj.elementOr) { 3656 | match = checkElementCondition(e, true); 3657 | if (match) { 3658 | break; 3659 | } 3660 | } 3661 | 3662 | if (!match) { 3663 | return false; 3664 | } 3665 | } 3666 | 3667 | if (Array.isArray(obj.elementAnd)) { 3668 | for (const e of obj.elementAnd) { 3669 | if (!checkElementCondition(e)) { 3670 | return false; 3671 | } 3672 | } 3673 | } 3674 | 3675 | return true; 3676 | }; 3677 | 3678 | const checkObject = async (object) => { 3679 | if (typeof object !== 'object') { 3680 | return; 3681 | } 3682 | 3683 | const checkData = {}; 3684 | const checkResult = checkConditions(object, checkData); 3685 | 3686 | if (!checkResult) { 3687 | const cleanObject = (obj) => { 3688 | if (typeof obj.ready === 'function' && obj.readyMulticall) { 3689 | obj._readyCalled = false; 3690 | } 3691 | 3692 | if (Array.isArray(obj.buttons)) { 3693 | for (const button of obj.buttons) { 3694 | if (button._button && !button._working) { 3695 | button._button.remove(); 3696 | delete button._button; 3697 | } 3698 | } 3699 | } 3700 | 3701 | if (Array.isArray(obj.conditions)) { 3702 | for (const condition of obj.conditions) { 3703 | cleanObject(condition); 3704 | } 3705 | } 3706 | }; 3707 | 3708 | cleanObject(object); 3709 | 3710 | return; 3711 | } 3712 | 3713 | if (typeof object.ready === 'function' && !object._readyCalled) { 3714 | log.debug('calling ready()...'); 3715 | 3716 | object._readyCalled = true; 3717 | 3718 | try { 3719 | const res = object.ready({ 3720 | self: object, 3721 | site: state.site, 3722 | matchedSelector: checkData.matchedSelector, 3723 | matchedElement: checkData.matchedElement 3724 | }); 3725 | 3726 | if (res instanceof Promise) { 3727 | await res; 3728 | } 3729 | } catch (e) { 3730 | log.error('ready() exception :', e); 3731 | } finally { 3732 | log.debug('ready() done'); 3733 | } 3734 | } 3735 | 3736 | if (Array.isArray(object.buttons)) { 3737 | for (const button of object.buttons) { 3738 | if (button._button) { 3739 | continue; 3740 | } 3741 | 3742 | const options = Object.assign({}, button); 3743 | 3744 | options._matchedSelector = checkData.matchedSelector; 3745 | options._matchedElement = checkData.matchedElement; 3746 | options._self = object; 3747 | options._button = button; 3748 | 3749 | button._button = await buttons.add(options); 3750 | } 3751 | } 3752 | 3753 | const getElementResult = (variable, method, callback) => { 3754 | if (typeof variable === 'string') { 3755 | let selector = checkData.matchedSelector ? variable.replace('{{element}}', checkData.matchedSelector) : variable; 3756 | const match = selector.match(/(.+)(%|@)(.+)$/); 3757 | 3758 | if (match) { 3759 | selector = match[1]; 3760 | method = match[2] + match[3]; 3761 | } 3762 | 3763 | const els = $J(selector); 3764 | 3765 | if (els.length) { 3766 | const name = method.substring(1); 3767 | 3768 | if (method[0] === '%') { 3769 | if (name === 'val') { 3770 | els.each((i, el) => { 3771 | callback($J(el).val()); 3772 | }); 3773 | } else if (name === 'html') { 3774 | els.each((i, el) => { 3775 | callback($J(el).val()); 3776 | }); 3777 | } else { 3778 | els.each((i, el) => { 3779 | callback($J(el).text()); 3780 | }); 3781 | } 3782 | } else { 3783 | els.each((i, el) => { 3784 | callback($J(el).attr(name)); 3785 | }); 3786 | } 3787 | } 3788 | } else if (typeof variable === 'function') { 3789 | const res = variable({ 3790 | self: object, 3791 | site: state.site, 3792 | matchedSelector: checkData.matchedSelector, 3793 | matchedElement: checkData.matchedElement 3794 | }); 3795 | 3796 | if (typeof res === 'string' || Array.isArray(res)) { 3797 | callback(res); 3798 | } 3799 | } else if (Array.isArray(variable)) { 3800 | for (const v of variable) { 3801 | getElementResult(v, method, callback); 3802 | } 3803 | } 3804 | }; 3805 | 3806 | if (!steam.initFailed && !object._steamTasksWorking && ( 3807 | (config.steamGroups && object.steamGroups) || 3808 | (config.steamAppWishlist && object.steamAppWishlist) || 3809 | (config.steamAppFollow && object.steamAppFollow) || 3810 | (config.steamAppAddToLibrary && object.steamAppAddToLibrary) 3811 | )) { 3812 | const groups = []; 3813 | const wishlist = []; 3814 | const follow = []; 3815 | const library = []; 3816 | 3817 | if (object.steamGroups) { 3818 | getElementResult(object.steamGroups, '@href', (res) => { 3819 | if (!Array.isArray(res)) { 3820 | res = [res]; 3821 | } 3822 | 3823 | for (const r of res) { 3824 | if (r && !buttons.isSteamGroupAdded(r) && !utils.getResolvedUrl(r) && (!state.site._buttonsFailed || !state.site._buttonsFailed.includes(r))) { 3825 | groups.push(r); 3826 | } 3827 | } 3828 | }); 3829 | } 3830 | 3831 | if (object.steamAppWishlist) { 3832 | getElementResult(object.steamAppWishlist, '@href', (res) => { 3833 | if (!Array.isArray(res)) { 3834 | res = [res]; 3835 | } 3836 | 3837 | for (const r of res) { 3838 | if (r && !buttons.isSteamAppWishlistAdded(r) && !utils.getResolvedUrl(r) && (!state.site._buttonsFailed || !state.site._buttonsFailed.includes(r))) { 3839 | wishlist.push(r); 3840 | } 3841 | } 3842 | }); 3843 | } 3844 | 3845 | if (object.steamAppFollow) { 3846 | getElementResult(object.steamAppFollow, '@href', (res) => { 3847 | if (!Array.isArray(res)) { 3848 | res = [res]; 3849 | } 3850 | 3851 | for (const r of res) { 3852 | if (r && !buttons.isSteamAppFollowAdded(r) && !utils.getResolvedUrl(r) && (!state.site._buttonsFailed || !state.site._buttonsFailed.includes(r))) { 3853 | follow.push(r); 3854 | } 3855 | } 3856 | }); 3857 | } 3858 | 3859 | if (object.steamAppAddToLibrary) { 3860 | getElementResult(object.steamAppAddToLibrary, '@href', (res) => { 3861 | if (!Array.isArray(res)) { 3862 | res = [res]; 3863 | } 3864 | 3865 | for (const r of res) { 3866 | if (r && !buttons.isSteamAppAddToLibraryAdded(r) && !utils.getResolvedUrl(r) && (!state.site._buttonsFailed || !state.site._buttonsFailed.includes(r))) { 3867 | library.push(r); 3868 | } 3869 | } 3870 | }); 3871 | } 3872 | 3873 | if (groups.length || wishlist.length || follow.length || library.length) { 3874 | object._steamTasksWorking = true; 3875 | 3876 | setTimeout(async () => { 3877 | const noti = notifications.info(i18n.get('steam-loading-tasks'), {timeout: 0}); 3878 | const promises = []; 3879 | const result = (res, id) => { 3880 | if (!res) { 3881 | if (!state.site._buttonsFailed) { 3882 | state.site._buttonsFailed = []; 3883 | } 3884 | 3885 | state.site._buttonsFailed.push(id); 3886 | } 3887 | }; 3888 | 3889 | for (const g of groups) { 3890 | promises.push(buttons.add({ 3891 | type: 'steam-group', 3892 | steamGroup: g 3893 | }).then((res) => result(res, g))); 3894 | } 3895 | 3896 | for (const w of wishlist) { 3897 | promises.push(buttons.add({ 3898 | type: 'steam-app-wishlist', 3899 | steamApp: w 3900 | }).then((res) => result(res, w))); 3901 | } 3902 | 3903 | for (const f of follow) { 3904 | promises.push(buttons.add({ 3905 | type: 'steam-app-follow', 3906 | steamApp: f 3907 | }).then((res) => result(res, f))); 3908 | } 3909 | 3910 | for (const l of library) { 3911 | promises.push(buttons.add({ 3912 | type: 'steam-app-add', 3913 | steamApp: l 3914 | }).then((res) => result(res, l))); 3915 | } 3916 | 3917 | await Promise.all(promises); 3918 | 3919 | noti.remove(); 3920 | 3921 | object._steamTasksWorking = false; 3922 | }); 3923 | } 3924 | } 3925 | 3926 | if (typeof object.steamKeys !== 'undefined') { 3927 | getElementResult(object.steamKeys, '%text', (res) => { 3928 | const addButtons = (keys) => { 3929 | if (!keys) { 3930 | return; 3931 | } 3932 | 3933 | for (const key of keys) { 3934 | if (key) { 3935 | buttons.add({ 3936 | type: 'steam-key', 3937 | steamKey: key 3938 | }); 3939 | } 3940 | } 3941 | }; 3942 | 3943 | if (typeof res === 'string') { 3944 | addButtons(steam.extractKeys(res)); 3945 | } else if (Array.isArray(res)) { 3946 | for (const r of res) { 3947 | addButtons(steam.extractKeys(r)); 3948 | } 3949 | } 3950 | }); 3951 | } 3952 | 3953 | if (Array.isArray(object.conditions)) { 3954 | for (const condition of object.conditions) { 3955 | await checkObject(condition); 3956 | } 3957 | } 3958 | }; 3959 | 3960 | await checkObject(state.site); 3961 | 3962 | setTimeout(checkSite, 1000); 3963 | }; 3964 | 3965 | if (window.location.href.includes('::entry_method')) { 3966 | return; 3967 | } 3968 | 3969 | for (const s of config.sites) { 3970 | if (checkStringCondition(s.host, state.host)) { 3971 | state.site = s; 3972 | break; 3973 | } 3974 | } 3975 | 3976 | if (!state.site) { 3977 | log.warn('site not found'); 3978 | 3979 | return; 3980 | } 3981 | 3982 | // If the site has overrided console, restore it 3983 | if (state.site.console) { 3984 | console = $J('').appendTo('body').get(0).contentWindow.console; 3985 | } 3986 | 3987 | const lang = navigator.language.replace(/-.+/, '').toLowerCase(); 3988 | 3989 | if (typeof i18n.langs[lang] === 'object') { 3990 | i18n.lang = i18n.langs[lang]; 3991 | i18n.code = lang; 3992 | } else { 3993 | i18n.lang = i18n.langs.default; 3994 | i18n.code = 'default'; 3995 | } 3996 | 3997 | const oldVersion = await $GM.getValue('version', false); 3998 | 3999 | if (oldVersion) { 4000 | // https://github.com/Rombecchi/version-compare 4001 | const versionCompare = function(v1, v2, options) { 4002 | const lexicographical = (options && options.lexicographical) || false; 4003 | const zeroExtend = (options && options.zeroExtend) || true; 4004 | let v1parts = (v1 || '0').split('.'); 4005 | let v2parts = (v2 || '0').split('.'); 4006 | 4007 | const isValidPart = function(x) { 4008 | return (lexicographical ? /^\d+[A-Za-zαß]*$/ : /^\d+[A-Za-zαß]?$/).test(x); 4009 | }; 4010 | 4011 | if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { 4012 | return NaN; 4013 | } 4014 | 4015 | if (zeroExtend) { 4016 | while (v1parts.length < v2parts.length) { 4017 | v1parts.push('0'); 4018 | } 4019 | 4020 | while (v2parts.length < v1parts.length) { 4021 | v2parts.push('0'); 4022 | } 4023 | } 4024 | 4025 | if (!lexicographical) { 4026 | v1parts = v1parts.map(function(x) { 4027 | const match = /[A-Za-zαß]/.exec(x); 4028 | return Number(match ? x.replace(match[0], '.' + x.charCodeAt(match.index)) : x); 4029 | }); 4030 | 4031 | v2parts = v2parts.map(function(x) { 4032 | const match = /[A-Za-zαß]/.exec(x); 4033 | return Number(match ? x.replace(match[0], '.' + x.charCodeAt(match.index)) : x); 4034 | }); 4035 | } 4036 | 4037 | for (let i = 0; i < v1parts.length; ++i) { 4038 | if (v2parts.length == i) { 4039 | return 1; 4040 | } 4041 | 4042 | if (v1parts[i] == v2parts[i]) { 4043 | continue; 4044 | } else if (v1parts[i] > v2parts[i]) { 4045 | return 1; 4046 | } else { 4047 | return -1; 4048 | } 4049 | } 4050 | 4051 | if (v1parts.length != v2parts.length) { 4052 | return -1; 4053 | } 4054 | 4055 | return 0; 4056 | }; 4057 | 4058 | if (versionCompare(oldVersion, version.string) < 0) { 4059 | notifications.info(i18n.get('gc-updated') + (version.changes[i18n.code] ? version.changes[i18n.code] : version.changes.default), {timeout: 0}); 4060 | } 4061 | } 4062 | 4063 | $GM.setValue('version', version.string); 4064 | 4065 | state.position = await $GM.getValue('position', 240); 4066 | state.maxHeight = await $GM.getValue('maxHeight', 206); 4067 | 4068 | log.info('start'); 4069 | 4070 | checkSite(); 4071 | })(); 4072 | })(); 4073 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Giveaway Companion 2 | ## English description 3 | This script adds useful features to game giveaway sites. Allows you to quickly complete/skip tasks, complete Steam tasks without leaving the giveaway page (join groups, add to wishlist, follow games), open Steam key activation page. 4 | 5 | The script does not complete subscriptions/reposts/likes on social networks, but such tasks can be completed automatically if they have no verification or if you have completed the same task before and have not cancelled the action. 6 | 7 | The script bar looks something like this (the set of buttons depends on the site and page): 8 | The script bar 9 | 10 | The script is inspired by [Giveaway Helper](https://github.com/Citrinate/giveawayHelper) and [GiveawayKiller](https://github.com/gekkedev/GiveawayKiller). 11 | 12 | [Changelog](CHANGELOG.md) 13 | 14 | **Disclaimer: the usage of this script may violate the Terms of Service of the sites it runs on. Use at your own risk.** 15 | 16 | ### Supported sites 17 | | Site | Features 18 | | :----------------- | :----------------- 19 | | grabfreegame.com | Complete simple tasks and confirm, join a Steam group, activate a key on Steam 20 | | bananatic.com | Complete simple tasks and confirm, join a Steam group, activate a key on Steam 21 | | gamingimpact.com | Complete simple tasks and confirm, join a Steam group, activate a key on Steam 22 | | whosgamingnow.net | Confirm tasks, join a Steam group, activate a key on Steam 23 | | gamehag.com | Complete simple tasks and confirm, join a Steam group, activate a key on Steam 24 | | gleam.io | Complete simple tasks and confirm, set a task timer to zero, join a Steam group, activate a key on Steam 25 | | giveaway.su | Join a Steam group, activate a key on Steam 26 | | keyjoker.com | Confirm tasks, join a Steam group, activate a key on Steam, opens a key activation page when you click a game image on [your keys page](https://www.keyjoker.com/account/keys) 27 | | key-hub.eu | Complete simple tasks, join a Steam group, add to Steam wishlist 28 | | givee.club | Confirm tasks, join a Steam group, add to Steam wishlist, follow a game on Steam 29 | | opquests.com | Confirm tasks, join a Steam group, add to Steam wishlist, follow a game on Steam, add a game to Steam library, activate a key on Steam 30 | 31 | ### Installation 32 | 1. Install one of the browser extensions to run user scripts. 33 | Tampermonkey: [Chrome](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/), [Opera](https://addons.opera.com/en/extensions/details/tampermonkey-beta/), [Edge](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) 34 | Violentmonkey: [Chrome](https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/), [Maxthon](https://extension.maxthon.com/detail/index.php?view_id=1680), [Edge](https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao) 35 | 2. Go [here](/../../raw/master/GiveawayCompanion.user.js). 36 | 3. Confirm installation of the script. 37 | 38 | Automatic updating of the script may require your confirmation. 39 | 40 | *** 41 | 42 | ## Русское описание 43 | Данный скрипт добавляет полезные функции на сайтах с раздачами игр. Позволяет быстро выполнять/пропускать задания, выполнять Steam задания не уходя со страницы раздачи (вступать в группы, добавлять в желаемое и подписываться на игры), переходить на страницу активации Steam ключа. 44 | 45 | Скрипт не выполняет подписки/репосты/лайки в социальных сетях, но подобные задания могут быть выполнены автоматически, если они не имеют проверки или если вы раньше уже выполняли такое же задание и не отменили действие. 46 | 47 | Панель скрипта выглядит примерно так (набор кнопок зависит от сайта и страницы): 48 | Панель скрипта 49 | 50 | Скрипт вдохновлён [Giveaway Helper](https://github.com/Citrinate/giveawayHelper) и [GiveawayKiller](https://github.com/gekkedev/GiveawayKiller). 51 | 52 | [Список изменений](CHANGELOG.md#%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA-%D0%B8%D0%B7%D0%BC%D0%B5%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9) 53 | 54 | **Отказ от ответственности: использование данного скрипта может нарушать правила сайтов, на которых он используется. Используйте на свой страх и риск.** 55 | 56 | ### Поддерживаемые сайты 57 | | Сайт | Функции 58 | | :----------------- | :----------------- 59 | | grabfreegame.com | Выполнение простых заданий и подтверждение, вступление в группу в Steam, активация ключа в Steam 60 | | bananatic.com | Выполнение простых заданий и подтверждение, вступление в группу в Steam, активация ключа в Steam 61 | | gamingimpact.com | Выполнение простых заданий и подтверждение, вступление в группу в Steam, активация ключа в Steam 62 | | whosgamingnow.net | Подтверждение заданий, вступление в группу в Steam, активация ключа в Steam 63 | | gamehag.com | Выполнение простых заданий и подтверждение, вступление в группу в Steam, активация ключа в Steam 64 | | gleam.io | Выполнение простых заданий и подтверждение, установка таймера заданий в ноль, вступление в группу в Steam, активация ключа в Steam 65 | | giveaway.su | Вступление в группу в Steam, активация ключа в Steam 66 | | keyjoker.com | Подтверждение заданий, вступление в группу в Steam, активация ключа в Steam, переход на страницу активации ключа при клике по изображению игры на [странице ваших ключей](https://www.keyjoker.com/account/keys) 67 | | key-hub.eu | Выполнение простых заданий, вступление в группу в Steam, добавление в список желаемого в Steam 68 | | givee.club | Подтверждение заданий, вступление в группу в Steam, добавление в список желаемого в Steam, подписка на игру в Steam 69 | | opquests.com | Подтверждение заданий, вступление в группу в Steam, добавление в список желаемого в Steam, подписка на игру в Steam, добавление игры в библиотеку Steam, активация ключа в Steam 70 | 71 | ### Установка 72 | 1. Установить одно из браузерных расширений для выполнения пользовательских скриптов. 73 | Tampermonkey: [Chrome](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ru), [Firefox](https://addons.mozilla.org/ru/firefox/addon/tampermonkey/), [Opera](https://addons.opera.com/ru/extensions/details/tampermonkey-beta/), [Edge](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd?hl=ru-RU) 74 | Violentmonkey: [Chrome](https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag?hl=ru), [Firefox](https://addons.mozilla.org/ru/firefox/addon/violentmonkey/), [Maxthon](https://extension.maxthon.com/detail/index.php?view_id=1680), [Edge](https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao?hl=ru-RU) 75 | 2. Перейти [сюда](/../../raw/master/GiveawayCompanion.user.js). 76 | 3. Подтвердить установку скрипта. 77 | 78 | При автоматическом обновлении скрипта может потребоваться ваше подтверждение. -------------------------------------------------------------------------------- /images/script_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longnull/GiveawayCompanion/c3e48beb4b9424076f030ebb6cc20de6828ee071/images/script_bar.png --------------------------------------------------------------------------------