├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.MD ├── docs ├── data │ └── channels.json ├── img │ ├── logo │ │ ├── one-s.ico │ │ ├── plus.png │ │ └── telegram.svg │ └── one-s │ │ ├── favorite.svg │ │ ├── functions.svg │ │ ├── history.svg │ │ ├── menu.svg │ │ ├── notify.svg │ │ ├── search.svg │ │ ├── window-list-left.png │ │ └── window-list-right.png ├── index.html ├── scripts │ ├── chanels.js │ └── links.js └── styles │ └── main.css └── tools └── updater ├── ConsoleUpdater ├── ConsoleUpdater.csproj └── Program.cs ├── Updater.sln ├── UpdaterLibrary.Tests ├── FakeUpdater.cs ├── IrrelevantTagsUpdateHandlerTest.cs └── UpdaterLibrary.Tests.csproj └── UpdaterLibrary ├── Models ├── ChannelModel.cs ├── MetadataColumnModel.cs ├── MetadataModel.cs ├── TagModel.cs └── UpdateDataModel.cs ├── Settings ├── SettingsManager.cs └── SettingsModel.cs ├── SiteUpdater ├── IUpdater.cs ├── LogEventModel.cs ├── LogEventType.cs └── UpdateParametrs.cs ├── TelegramBot └── TelegramApi.cs ├── Tools ├── ArrayExtension.cs └── ReflectionHelper.cs ├── UpdateHandlers ├── ChannelInfoUpdateHandler.cs ├── IUpdateHandler.cs ├── IrrelevantTagsUpdateHandler.cs ├── MemberCountUpdateHandler.cs └── TempUpdateHandler.cs ├── Updater.cs ├── UpdaterLibrary.csproj └── appsettings.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Файл с настройками и токеном 2 | tools/updater/UpdaterLibrary/appsettings.json 3 | 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | ## 7 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:5500", 12 | "webRoot": "${workspaceFolder}", 13 | "file": "${workspaceRoot}/site/index.html" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.language": "en,ru", 3 | "cSpell.ignorePaths": [ 4 | "*.json", 5 | ], 6 | "cSpell.words": [ 7 | "SeiOkami" 8 | ], 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Vitaliy Chernenko 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 | [![OpenYellow](https://img.shields.io/endpoint?url=https://openyellow.org/data/badges/4/800923299.json)](https://openyellow.org/grid?data=top&repo=800923299) 2 | 3 | # Реестр 1С в Telegram 4 | 5 | Репозиторий реестра всех каналов, чатов и т.д. по теме 1С в Telegram 6 | 7 | Список ссылок: 8 | - [Опубликованный реестр](https://SeiOkami.github.io/links-one-s/) 9 | - [Предложения по улучшению](https://github.com/SeiOkami/links-one-s/issues) 10 | - [Автор](https://github.com/SeiOkami) 11 | - [Личка в Telegram](https://t.me/SeiOkami) 12 | 13 | ## Как добавить канал 14 | Для добавления нового канала можно сделать одним из следующих способов: 15 | - Написать мне в личку Telegram 16 | - Зарегистрировать issue на странице предложений 17 | - Самому создать пулл реквест. Для этого необходимо добавить свой канал в конце списка `channels` файла [channels.json](/docs/data/channels.json). Указать нужно URL и ID. Остальная информация будет обработана автоматически 18 | 19 | ## Структура проекта 20 | Проект содержит: 21 | - [docs](/docs/) - Статический сайт, опубликованный на GitHub Pages 22 | - [channels.json](/docs/data/channels.json) - список всех данных сайта (теги, выводимые колонки, каналы, чаты и т.д.) 23 | - [updater](/tools/updater/) - C# проект для автоматического обновления данных реестра из Telegram 24 | -------------------------------------------------------------------------------- /docs/img/logo/one-s.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeiOkami/links-one-s/7509e914e936f38e73413b7cda7b7cd91da0f0de/docs/img/logo/one-s.ico -------------------------------------------------------------------------------- /docs/img/logo/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeiOkami/links-one-s/7509e914e936f38e73413b7cda7b7cd91da0f0de/docs/img/logo/plus.png -------------------------------------------------------------------------------- /docs/img/logo/telegram.svg: -------------------------------------------------------------------------------- 1 | Telegram_logo -------------------------------------------------------------------------------- /docs/img/one-s/favorite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/img/one-s/functions.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/img/one-s/history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/img/one-s/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/img/one-s/notify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/img/one-s/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/img/one-s/window-list-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeiOkami/links-one-s/7509e914e936f38e73413b7cda7b7cd91da0f0de/docs/img/one-s/window-list-left.png -------------------------------------------------------------------------------- /docs/img/one-s/window-list-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeiOkami/links-one-s/7509e914e936f38e73413b7cda7b7cd91da0f0de/docs/img/one-s/window-list-right.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 1С в Telegram 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 |
Список телеграм каналов и чатов по 1С в Telegram
25 | 26 | 27 | 28 | 29 |
Черненко Виталий (SeiOkami)
30 | 31 |
32 | 33 |
34 | 35 |
Репозиторий
36 |
Как добавить канал
37 |
Идеи и предложения
38 |
Желтый Чайник 1С
39 |
Автор (GitHub)
40 |
Автор (Telegram)
41 | 42 |
43 | 44 |
45 |
46 | 47 |
48 | 49 |
Список каналов и чатов
50 | 51 |
52 | 53 |
54 | 55 | 56 |
57 | 58 |
59 | 60 |
61 | 62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/scripts/chanels.js: -------------------------------------------------------------------------------- 1 | { 2 | const classColumn = "column-"; // Префикс стиля колонки 3 | const searchInput = document.getElementById('searchInput'); // Поле строки поиска 4 | const table = document.getElementById('channels'); // Таблица каналов на странице 5 | let channels = undefined; // Коллекция каналов из файла 6 | let tags = undefined; // Коллекция тегов из файла 7 | let metadata = undefined; // Метаданные таблицы каналов из файла 8 | let filterTags = []; // Теги, выбранные пользователем для просмотра 9 | let searchText = ""; // Текущий текст в поле поиска 10 | 11 | // Создание таблицы каналов на основе прочитанных данных из файла 12 | function initialTable() { 13 | 14 | initialTable_columns(); 15 | initialTable_rows(); 16 | 17 | } 18 | 19 | // Создание заголовков колонок таблицы каналов 20 | function initialTable_columns(){ 21 | 22 | const thead = document.createElement('thead'); 23 | const headerRow = document.createElement('tr'); 24 | 25 | let index = 0; 26 | metadata.Columns.forEach(column => { 27 | const th = document.createElement('th'); 28 | th.classList.add(classColumn + column.Name); 29 | const thisIndex = index; 30 | th.onclick = () => sortTable(thisIndex); 31 | th.textContent = column.Presentation; 32 | headerRow.appendChild(th); 33 | index++; 34 | }); 35 | 36 | thead.appendChild(headerRow); 37 | table.appendChild(thead); 38 | 39 | } 40 | 41 | // Создание строк таблицы каналов 42 | function initialTable_rows(){ 43 | 44 | const tbody = document.createElement('tbody'); 45 | 46 | channels.forEach(item => { 47 | const row = document.createElement('tr'); 48 | 49 | metadata.Columns.forEach(column => { 50 | 51 | const columnName = column.Name; 52 | const cell = document.createElement('td'); 53 | cell.classList.add(classColumn + columnName); 54 | 55 | const value = item[columnName]; 56 | 57 | if (columnName === 'Tags') { 58 | value.forEach(tagName => { 59 | addTagElement(tagName, cell, false); 60 | }); 61 | } else { 62 | cell.innerHTML = makeLinksClickable(value); 63 | } 64 | 65 | row.appendChild(cell); 66 | }) 67 | 68 | tbody.appendChild(row); 69 | 70 | }); 71 | 72 | table.appendChild(tbody); 73 | 74 | } 75 | 76 | // Функция делает ссылки в ячейке кликабельными 77 | // Метод сделан при помощи claude, можно улучшить 78 | function makeLinksClickable(text) { 79 | 80 | // Разделяем текст на массив строк 81 | const lines = text.toString().split('\n'); 82 | 83 | // Обрабатываем каждую строку 84 | const formattedLines = lines.map(line => { 85 | // Регулярное выражение для URL-ссылок 86 | const urlPattern = /(https?:\/\/[^\s]+)/g; 87 | 88 | // Регулярное выражение для имен пользователей 89 | const userPattern = /(? `${url}`; 93 | 94 | // Функция для замены имен пользователей 95 | const replaceUser = (match, username) => `@${username}`; 96 | 97 | // Замена URL-ссылок 98 | line = line.replace(urlPattern, replaceUrl); 99 | 100 | // Замена имен пользователей 101 | line = line.replace(userPattern, replaceUser); 102 | 103 | return line; 104 | }); 105 | 106 | // Объединяем строки обратно в один текст 107 | const formattedText = formattedLines.join('
'); 108 | 109 | return formattedText; 110 | } 111 | 112 | // Добавляет элемент тега в указанный объект 113 | function addTagElement(tagName, parent, deleteExisting){ 114 | const tag = tags.find(el => el.Name === tagName); 115 | if (tag) { 116 | const tagElement = document.createElement('span'); 117 | tagElement.classList.add('tag'); 118 | tagElement.textContent = tagName; 119 | tagElement.style.backgroundColor = `${tag.Color}`; 120 | tagElement.title = tag?.Description || ''; 121 | tagElement.onclick = () => addFilterTag(tagName, deleteExisting); 122 | parent.appendChild(tagElement); 123 | return tagElement; 124 | } else { 125 | console.error(`Not found tag: ${tagName}`); 126 | } 127 | } 128 | 129 | // Корректирует строку под правила именования классов 130 | function convertToClassName(str) { 131 | return str.trim().replace(/\s+/g, '_').toLowerCase(); 132 | } 133 | 134 | // Добавляет указанный тег в фильтры или удаляет из них 135 | function addFilterTag(tagName, deleteExisting){ 136 | const panel = document.getElementById("filters-panel"); 137 | const id = "tag-filter-" + convertToClassName(tagName); 138 | let elem = document.getElementById(id); 139 | if (elem == undefined){ 140 | elem = addTagElement(tagName, panel, true); 141 | elem.id = id; 142 | elem.classList.add('tag-filter'); 143 | filterTags.push(tagName); 144 | } else if (deleteExisting) { 145 | elem.remove(); 146 | filterTags = filterTags.filter(item => item !== tagName); 147 | } 148 | 149 | updateFilter(); 150 | } 151 | 152 | // Загрузка данных из файла channels.json 153 | function loadFile(){ 154 | fetch('data/channels.json') 155 | .then(response => response.json()) 156 | .then(data => { 157 | 158 | channels = data.Channels; 159 | tags = data.Tags; 160 | metadata = data.Metadata; 161 | 162 | initialTable(); 163 | }) 164 | .catch(error => console.error('Error file load:', error)); 165 | } 166 | 167 | // Настройка поля ввода поиска по таблице 168 | function initialSearchInput() { 169 | searchInput.addEventListener('input', updateFilter); 170 | } 171 | 172 | // Проверяет соответствие строки условиям отбора 173 | function rowVisible(row){ 174 | 175 | return rowVisible_tag(row) && rowVisible_input(row); 176 | 177 | } 178 | 179 | // Проверяет соответствие строки выбранным тегам 180 | function rowVisible_tag(row){ 181 | 182 | let result = true; 183 | 184 | if (filterTags.length) { 185 | const tagElements = row.querySelectorAll('.column-Tags .tag'); 186 | result = Array.from(filterTags).every(filterTag => 187 | Array.from(tagElements).some(element => element.textContent === filterTag)); 188 | } 189 | 190 | return result; 191 | 192 | } 193 | 194 | // Проверяет соответствие строки поиску 195 | function rowVisible_input(row){ 196 | 197 | if (searchText === '') { 198 | return true; 199 | } else { 200 | return Array.from(row.children) 201 | .map(cell => cell.textContent.toLowerCase()) 202 | .some(data => data.includes(searchText)); 203 | } 204 | 205 | } 206 | 207 | // Обновляет фильтр строк таблицы 208 | function updateFilter(){ 209 | searchText = searchInput.value.toLowerCase(); 210 | Array.from(table.querySelectorAll('tbody tr')).forEach(row => { 211 | row.style.display = rowVisible(row) ? '' : 'none'; 212 | }); 213 | } 214 | 215 | // Сортировка по колонке по индексу (при клике на заголовок колонки) 216 | // Метод сделан при помощи claude, можно улучшить 217 | function sortTable(columnIndex) { 218 | 219 | const rows = Array.from(table.getElementsByTagName("tr")); 220 | 221 | rows.shift(); // Убираем заголовок таблицы из массива 222 | 223 | // Определяем тип сортировки (по возрастанию или убыванию) 224 | let sortOrder = 1; 225 | if (table.rows[0].cells[columnIndex].classList.contains("sorted-asc")) { 226 | sortOrder = -1; 227 | } 228 | 229 | // Сортируем строки таблицы 230 | rows.sort((a, b) => { 231 | const aValue = a.cells[columnIndex].textContent.trim(); 232 | const bValue = b.cells[columnIndex].textContent.trim(); 233 | if (isNaN(aValue) || isNaN(bValue)) { 234 | return sortOrder * aValue.localeCompare(bValue); 235 | } else { 236 | return sortOrder * (parseInt(aValue) - parseInt(bValue)); 237 | } 238 | }); 239 | 240 | // Удаляем текущие строки из таблицы 241 | while (table.rows.length > 1) { 242 | table.deleteRow(1); 243 | } 244 | 245 | // Вставляем отсортированные строки обратно в таблицу 246 | const tbody = table.getElementsByTagName("tbody")[0]; 247 | rows.forEach(row => { 248 | tbody.appendChild(row); 249 | }); 250 | 251 | // Удаляем классы сортировки из всех заголовков столбцов 252 | Array.from(table.getElementsByTagName("th")).forEach(th => { 253 | th.classList.remove("sorted-asc"); 254 | th.classList.remove("sorted-desc"); 255 | }); 256 | 257 | // Добавляем класс сортировки к выбранному заголовку столбца 258 | const selectedColumnHeader = table.rows[0].cells[columnIndex]; 259 | if (sortOrder === 1) { 260 | selectedColumnHeader.classList.add("sorted-asc"); 261 | } else { 262 | selectedColumnHeader.classList.add("sorted-desc"); 263 | } 264 | } 265 | 266 | { //Initial 267 | 268 | loadFile(); 269 | 270 | initialSearchInput(); 271 | 272 | } 273 | } -------------------------------------------------------------------------------- /docs/scripts/links.js: -------------------------------------------------------------------------------- 1 | 2 | { 3 | const urlAuthorGithub = "https://github.com/SeiOkami"; 4 | const urlRepository = urlAuthorGithub + "/links-one-s"; 5 | const urlHowAdd = urlRepository + "#Как-добавить-канал"; 6 | const urlIssues = urlRepository + "/issues"; 7 | const urlAuthorTelegram = "https://t.me/SeiOkami"; 8 | const urlJuniorOneS = "https://t.me/JuniorOneS"; 9 | 10 | function openRepository(){ 11 | openUrl(urlRepository); 12 | } 13 | 14 | function openHowAdd(){ 15 | openUrl(urlHowAdd); 16 | } 17 | 18 | function openIssues(){ 19 | openUrl(urlIssues); 20 | } 21 | 22 | function openAuthorGithub(){ 23 | openUrl(urlAuthorGithub); 24 | } 25 | 26 | function openAuthorTelegram(){ 27 | openUrl(urlAuthorTelegram); 28 | } 29 | 30 | function openJuniorOneS(){ 31 | openUrl(urlJuniorOneS); 32 | } 33 | 34 | function openUrl(url){ 35 | window.open(url, '_blank'); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /docs/styles/main.css: -------------------------------------------------------------------------------- 1 | 2 | /* ПЕРЕМЕННЫЕ */ 3 | 4 | :root { 5 | --color-text: #4b4b4b; 6 | --border-func-color: #aea575; 7 | --border-color: #dbdada; 8 | --background-main: #fbed9e; 9 | --shadow-color: rgba(233, 233, 233, 1); 10 | --first-row-color: #ffffff; 11 | --second-row-color: #fafafa; 12 | --th-color: #f2f2f2; 13 | } 14 | 15 | 16 | /* ТЕЛО */ 17 | 18 | body { 19 | color: var(--color-text); 20 | font-family: '', Arial, sans-serif; 21 | font-size: 10pt; 22 | cursor: default; 23 | overflow: hidden; 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | bottom: 0; 28 | left: 0; 29 | margin: 0; 30 | height: 100%; 31 | } 32 | 33 | .flex { 34 | display: flex; 35 | } 36 | 37 | /* ШАПКА СТРАНИЦЫ */ 38 | 39 | header, .functions { 40 | background: var(--background-main); 41 | border-bottom: 1px solid var(--border-func-color); 42 | align-items: center; 43 | display: flex; 44 | } 45 | 46 | header { 47 | height: 31px; 48 | } 49 | 50 | .functions { 51 | padding: 0px 10px; 52 | height: 52px; 53 | } 54 | 55 | .function { 56 | padding-left: 7px; 57 | padding-right: 10px; 58 | } 59 | 60 | .function:hover { 61 | cursor: pointer; 62 | text-decoration: underline; 63 | } 64 | 65 | .header_logo { 66 | align-items: center; 67 | display: flex; 68 | background-color: #f4e8a0; 69 | align-items: center; /* Вертикальное выравнивание */ 70 | justify-content: center; /* Горизонтальное выравнивание */ 71 | } 72 | 73 | .header-right-img{ 74 | width: 16px; 75 | height: 16px; 76 | padding: 5px; 77 | } 78 | 79 | .header-left-img{ 80 | width: 20px; 81 | height: 20px; 82 | } 83 | 84 | /* ТАБЛИЦА КАНАЛОВ */ 85 | 86 | #div_main{ 87 | padding: 1px 16px; 88 | } 89 | 90 | #div_channels{ 91 | border: solid 1px var(--border-color); 92 | } 93 | 94 | .channels_header { 95 | margin: 5px 0px; 96 | height: 28px; 97 | } 98 | 99 | table { 100 | border-collapse: collapse; 101 | width: 100%; 102 | border-radius: 2px; 103 | overflow: auto; /* Добавление полос прокрутки при необходимости */ 104 | display: block; /* Устанавливает таблицу как блочный элемент */ 105 | height: calc(100vh - 200px); 106 | flex-grow: 1; 107 | table-layout: auto; /* Устанавливает фиксированную ширину для ячеек */ 108 | border-collapse: collapse; /* Убирает двойные границы между ячейками */ 109 | box-sizing: border-box; 110 | } 111 | 112 | th.sorted-asc, 113 | th.sorted-desc{ 114 | padding-right: 20px; /* Отступ справа для размещения стрелочки */ 115 | } 116 | 117 | th.sorted-asc::after, 118 | th.sorted-desc::after{ 119 | position: absolute; /* Абсолютное позиционирование для стрелочки */ 120 | right: 5px; /* Отступ стрелочки от правого края */ 121 | top: 50%; /* Выравнивание стрелочки по вертикали */ 122 | transform: translateY(-50%); /* Сдвиг стрелочки вверх на 50% ее высоты */ 123 | } 124 | 125 | th.sorted-asc::after{ 126 | content: "↓"; 127 | } 128 | 129 | th.sorted-desc::after{ 130 | content: "↑"; 131 | } 132 | 133 | #searchInput { 134 | width: 100%; 135 | border: 1px solid var(--border-func-color); 136 | border-radius: 3px; 137 | } 138 | 139 | table tr:nth-child(even) { 140 | background-color: var(--second-row-color); /* Цвет фона для четных строк */ 141 | } 142 | 143 | table tr:nth-child(odd) { 144 | background-color: var(--first-row-color); /* Цвет фона для нечетных строк */ 145 | } 146 | 147 | th, td { 148 | border: 1px solid var(--border-color); 149 | padding: 8px; 150 | text-align: left; 151 | word-wrap: break-word; /* Переносит слова на новую строку, если они не помещаются в ячейку */ 152 | padding: 5px; /* Добавляет небольшой отступ внутри ячеек */ 153 | max-width: 500px; /* Позволяет ячейке сжиматься при необходимости */ 154 | } 155 | 156 | th { 157 | cursor: pointer; 158 | background-color: var(--th-color); 159 | font-weight: normal; 160 | height: 28px; 161 | padding-left: 8px; 162 | position: sticky; 163 | top: 0; 164 | } 165 | 166 | .header_title{ 167 | margin-left: 4px; 168 | flex: 1 1 auto; 169 | } 170 | 171 | .btn-arrow { 172 | margin: 0 -1px; 173 | height: inherit; 174 | padding: 2px 9px; 175 | border: 1px solid rgba(0, 0, 0, 0.3); 176 | box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2); 177 | } 178 | 179 | .channels_commands{ 180 | margin-bottom: 5px; 181 | } 182 | 183 | .margin-right{ 184 | margin-right: 8px; 185 | } 186 | 187 | button{ 188 | height: 24px; 189 | padding: 12px 10px; 190 | border: 1px solid rgba(0, 0, 0, 0.3); 191 | border-radius: 2px; 192 | color: var(--color-text); 193 | background-color: #fff; 194 | display: inline-flex; 195 | align-items: center; 196 | box-shadow: inset 0 -1px 3px rgb(230, 230, 230), 197 | 0 0 0 rgb(214, 214, 214); 198 | } 199 | 200 | button:hover{ 201 | cursor: pointer; 202 | border: 1px solid rgba(0, 0, 0, 0.6); 203 | } 204 | 205 | .topLineBox { 206 | font-size: 14pt; 207 | position: relative; 208 | width: 100%; 209 | min-width: 2em; 210 | height: 1.5em; 211 | line-height: 1.5em; 212 | padding: 2px 10px; 213 | vertical-align: middle; 214 | } 215 | 216 | .btn-arrow:before { 217 | content: ''; 218 | position: absolute; 219 | height: 100%; 220 | bottom: -100%; 221 | right: -50%; 222 | left: -50%; 223 | box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.1); 224 | } 225 | 226 | .column-ID, .column-Actual { 227 | display: none; 228 | } 229 | 230 | .tag { 231 | padding: 1px 5px; 232 | margin: 10px 3px; 233 | border-radius: 3px; 234 | cursor: pointer; 235 | } 236 | 237 | .tag-filter::after{ 238 | content: " ✖"; 239 | } 240 | -------------------------------------------------------------------------------- /tools/updater/ConsoleUpdater/ConsoleUpdater.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tools/updater/ConsoleUpdater/Program.cs: -------------------------------------------------------------------------------- 1 | using UpdaterLibrary; 2 | using UpdaterLibrary.SiteUpdater; 3 | using UpdaterLibrary.Tools; 4 | 5 | namespace ConsoleUpdater; 6 | 7 | static class Program 8 | { 9 | static async Task Main(string[] args) 10 | { 11 | 12 | string filterChanel = ""; 13 | 14 | Console.WriteLine("Обновление сайта с ресурсами по теме 1С".ToUpper()); 15 | Console.WriteLine("---------------------------------------"); 16 | Console.WriteLine("1. Обновить все данные"); 17 | Console.WriteLine("2. Обновить по каналу"); 18 | Console.WriteLine("3. Выйти"); 19 | 20 | while (true) 21 | { 22 | var key = Console.ReadKey(); 23 | if (key.KeyChar == '1') 24 | { 25 | Console.WriteLine("\n Обновление всех данных"); 26 | break; 27 | } else if (key.KeyChar == '2') { 28 | Console.Write("\n Имя канала: "); 29 | filterChanel = Console.ReadLine() ?? ""; 30 | if (!string.IsNullOrEmpty(filterChanel)) 31 | break; 32 | } 33 | else if (key.KeyChar == '3') 34 | { 35 | return; 36 | } 37 | } 38 | 39 | var updater = new Updater(); 40 | updater.AddLogHandler(AddToLog); 41 | updater.Parametrs.ChannelUserName = filterChanel; 42 | await updater.UpdateAsync(); 43 | 44 | Console.WriteLine($"Всего каналов: {updater.Data.Channels.Count}"); 45 | Console.WriteLine($"Всего тегов: {updater.Data.Tags.Count}"); 46 | 47 | Console.ReadLine(); 48 | 49 | } 50 | public static void AddToLog(string message, object obj, LogEventType type) 51 | { 52 | if (type == LogEventType.Error) 53 | Console.ForegroundColor = ConsoleColor.Red; 54 | else 55 | Console.ForegroundColor = ConsoleColor.Green; 56 | 57 | Console.WriteLine($"[{DateTime.Now}] {obj.ToStringRecurs()}: {message}"); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /tools/updater/Updater.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34728.123 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleUpdater", "ConsoleUpdater\ConsoleUpdater.csproj", "{328D96DF-27E7-49A9-846B-BA3FABDDB673}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UpdaterLibrary", "UpdaterLibrary\UpdaterLibrary.csproj", "{3D5B5F55-3AFE-4134-970F-9F9C25ACF9F1}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {328D96DF-27E7-49A9-846B-BA3FABDDB673}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {328D96DF-27E7-49A9-846B-BA3FABDDB673}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {328D96DF-27E7-49A9-846B-BA3FABDDB673}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {328D96DF-27E7-49A9-846B-BA3FABDDB673}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {3D5B5F55-3AFE-4134-970F-9F9C25ACF9F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {3D5B5F55-3AFE-4134-970F-9F9C25ACF9F1}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {3D5B5F55-3AFE-4134-970F-9F9C25ACF9F1}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {3D5B5F55-3AFE-4134-970F-9F9C25ACF9F1}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {2B2F8A1F-18F6-4080-BBBD-27FB036C60BA} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary.Tests/FakeUpdater.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Types; 2 | using UpdaterLibrary.Models; 3 | using UpdaterLibrary.SiteUpdater; 4 | 5 | namespace UpdaterLibrary.Tests; 6 | internal class FakeUpdater : IUpdater 7 | { 8 | public UpdateDataModel Data { get; set; } = new(); 9 | public Dictionary Log { get; set; } = new(); 10 | 11 | public void AddToLog(string message, object obj, LogEventType type = LogEventType.Change) 12 | { 13 | throw new NotImplementedException(); 14 | } 15 | 16 | public Task GetChatAsync(ChannelModel channel) 17 | { 18 | throw new NotImplementedException(); 19 | } 20 | 21 | public Task GetMemberCountAsync(ChannelModel channel) 22 | { 23 | throw new NotImplementedException(); 24 | } 25 | 26 | public string LogText() 27 | { 28 | throw new NotImplementedException(); 29 | } 30 | 31 | public Task UpdateAsync() 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | 36 | public void UpdateChannelTag(ChannelModel channel, string TagName, bool Add) 37 | { 38 | throw new NotImplementedException(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary.Tests/IrrelevantTagsUpdateHandlerTest.cs: -------------------------------------------------------------------------------- 1 | using UpdaterLibrary.Models; 2 | using UpdaterLibrary.UpdateHandlers; 3 | 4 | namespace UpdaterLibrary.Tests; 5 | 6 | [TestClass] 7 | public class IrrelevantTagsUpdateHandlerTest 8 | { 9 | [TestMethod] 10 | public async Task TestUpdateAsync() 11 | { 12 | var updater = new FakeUpdater(); 13 | updater.Data.Tags.Add(new TagModel(){ 14 | Name = "test", 15 | }); 16 | 17 | updater.Data.Channels.Add(new ChannelModel(){ 18 | Tags = ["test"] 19 | }); 20 | 21 | var handler = new IrrelevantTagsUpdateHandler(); 22 | await handler.UpdateAsync(updater); 23 | 24 | 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary.Tests/UpdaterLibrary.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/Models/ChannelModel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using UpdaterLibrary.Settings; 3 | 4 | namespace UpdaterLibrary.Models; 5 | 6 | public class ChannelModel 7 | { 8 | 9 | [JsonIgnore] 10 | public string UserName { 11 | get { 12 | return Url.Replace(SettingsManager.TELEGRAM_URL, "").Split('?')[0]; 13 | } 14 | set { 15 | Url = SettingsManager.TELEGRAM_URL + value; 16 | } 17 | } 18 | 19 | public long ID { get; set; } 20 | [JsonRequired] 21 | public string Url { get; set; } = string.Empty; 22 | public bool IsChannel { get; set; } 23 | public string Name { get; set; } = string.Empty; 24 | public string Description { get; set; } = string.Empty; 25 | public string Contact { get; set; } = string.Empty; 26 | public DateTime Actual { get; set; } 27 | public int MemberCount { get; set; } 28 | public string Comment { get; set; } = string.Empty; 29 | public List Tags { get; set; } = new(); 30 | 31 | public override string ToString() => UserName; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/Models/MetadataColumnModel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace UpdaterLibrary.Models; 4 | 5 | public class MetadataColumnModel 6 | { 7 | [JsonRequired] 8 | public string Name { get; set; } = string.Empty; 9 | 10 | [JsonRequired] 11 | public string Presentation { get; set; } = string.Empty; 12 | } 13 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/Models/MetadataModel.cs: -------------------------------------------------------------------------------- 1 | namespace UpdaterLibrary.Models; 2 | 3 | public class MetadataModel 4 | { 5 | public List Columns { get; set; } = new(); 6 | } 7 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/Models/TagModel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace UpdaterLibrary.Models; 4 | 5 | public class TagModel 6 | { 7 | [JsonRequired] 8 | public string Name { get; set; } = string.Empty; 9 | public string Description { get; set; } = string.Empty; 10 | public string Color { get; set; } = string.Empty; 11 | public override string ToString() => Name; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/Models/UpdateDataModel.cs: -------------------------------------------------------------------------------- 1 | namespace UpdaterLibrary.Models; 2 | 3 | public class UpdateDataModel 4 | { 5 | public MetadataModel Metadata { get; set; } = new(); 6 | public List Tags { get; set; } = new(); 7 | public List Channels { get; set; } = new(); 8 | } 9 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/Settings/SettingsManager.cs: -------------------------------------------------------------------------------- 1 | namespace UpdaterLibrary.Settings; 2 | using Newtonsoft.Json; 3 | 4 | public static class SettingsManager 5 | { 6 | private const string SETTINGS_FILE_NAME = "appsettings.json"; 7 | private const string CHANNELS_FILE_PATH = "data/channels.json"; 8 | public const string TELEGRAM_URL = $"https://t.me/"; 9 | public const string TAG_NAME_BEGIN = "Малыши (<500)"; 10 | public const string TAG_NAME_CLOSE = "Закрыт"; 11 | public const string TAG_NAME_CHANNEL = "Канал"; 12 | public const string TAG_NAME_CHAT = "Чат"; 13 | public const string TAG_NAME_BOT = "Бот"; 14 | public const string TAG_NAME_USER = "Пользователь"; 15 | public const int MAX_LENGTH_MESSAGE = 3000; 16 | public const int MAX_NUMBER_ATTEMPTS = 3; 17 | public const string ERROR_CHAT_NOT_FOUND = "Bad Request: chat not found"; 18 | public const string ERROR_CHAT_SUPERGROUP = "Forbidden: bot is not a member of the supergroup chat"; 19 | 20 | private static SettingsModel Instance { get; set; } 21 | 22 | public static string PathToFileChannels { get; private set; } 23 | public static string TelegramBotToken => Instance.TelegramBotToken; 24 | public static int DelayRequestsApi => Instance.DelayRequestsApi; 25 | public static List Subscribers => Instance.Subscribers; 26 | 27 | static SettingsManager() 28 | { 29 | Instance = ReadSettingsFile(); 30 | PathToFileChannels = FullPathToFile( 31 | false, 32 | Instance.PathToSite, 33 | CHANNELS_FILE_PATH); 34 | } 35 | 36 | private static SettingsModel ReadSettingsFile() 37 | { 38 | var filePath = FullPathToFile(true, SETTINGS_FILE_NAME); 39 | 40 | if (!File.Exists(filePath)) 41 | { 42 | throw new FileNotFoundException($"Файл настроек не найден: {filePath}"); 43 | } 44 | 45 | using (var reader = new StreamReader(filePath)) 46 | { 47 | var fileContents = reader.ReadToEnd(); 48 | var result = JsonConvert.DeserializeObject(fileContents); 49 | if (result is null) 50 | throw new FileLoadException($"Файл настроек пустой: {filePath}"); 51 | else 52 | return result; 53 | } 54 | } 55 | 56 | private static string FullPathToFile(bool isRelative, params string[] paths) 57 | { 58 | var path = Path.Combine(paths); 59 | if (isRelative) 60 | { 61 | path = Path.Combine(Directory.GetCurrentDirectory(), path); 62 | path = Path.GetFullPath(path); 63 | } 64 | return path; 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/Settings/SettingsModel.cs: -------------------------------------------------------------------------------- 1 | namespace UpdaterLibrary.Settings; 2 | public class SettingsModel 3 | { 4 | public string TelegramBotToken { get; set; } = string.Empty; 5 | public string PathToSite { get; set; } = string.Empty; 6 | public int DelayRequestsApi { get; set; } 7 | public List Subscribers { get; set; } = new(); 8 | } 9 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/SiteUpdater/IUpdater.cs: -------------------------------------------------------------------------------- 1 | using TelegramTypes = Telegram.Bot.Types; 2 | using UpdaterLibrary.Models; 3 | 4 | namespace UpdaterLibrary.SiteUpdater; 5 | 6 | public interface IUpdater 7 | { 8 | public UpdateDataModel Data { get; set; } 9 | public Dictionary Log { get; set; } 10 | public UpdateParametrs Parametrs { get; set; } 11 | public Task UpdateAsync(); 12 | public bool ChannelUpdateRequired(ChannelModel channel); 13 | public void UpdateChannelTag(ChannelModel channel, string TagName, bool Add); 14 | public void AddLogHandler(Action handler); 15 | public void AddToLog(string message, object obj, LogEventType type = LogEventType.Change); 16 | public string LogText(LogEventType? filterType = null); 17 | public Task GetMemberCountAsync(ChannelModel channel); 18 | public Task GetChatAsync(ChannelModel channel); 19 | } 20 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/SiteUpdater/LogEventModel.cs: -------------------------------------------------------------------------------- 1 | namespace UpdaterLibrary.SiteUpdater; 2 | 3 | public class LogEventModel 4 | { 5 | public List Objects { get; set; } = new(); 6 | public LogEventType Type { get; set; } 7 | public string Text { get; set; } = string.Empty; 8 | } 9 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/SiteUpdater/LogEventType.cs: -------------------------------------------------------------------------------- 1 | namespace UpdaterLibrary.SiteUpdater; 2 | 3 | public enum LogEventType 4 | { 5 | Change, 6 | Error 7 | } 8 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/SiteUpdater/UpdateParametrs.cs: -------------------------------------------------------------------------------- 1 | namespace UpdaterLibrary.SiteUpdater; 2 | 3 | public class UpdateParametrs 4 | { 5 | public long ChannelID { get; set; } 6 | public string ChannelUserName { get; set; } = string.Empty; 7 | public bool SaveChanges { get; set; } = true; 8 | } 9 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/TelegramBot/TelegramApi.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.RegularExpressions; 3 | using Telegram.Bot; 4 | using Telegram.Bot.Exceptions; 5 | using Telegram.Bot.Types; 6 | using UpdaterLibrary.Models; 7 | using UpdaterLibrary.Settings; 8 | using UpdaterLibrary.SiteUpdater; 9 | using UpdaterLibrary.Tools; 10 | 11 | namespace UpdaterLibrary.TelegramBot; 12 | public class TelegramApi 13 | { 14 | private readonly TelegramBotClient _telegram = new(SettingsManager.TelegramBotToken); 15 | 16 | private readonly IUpdater _updater; 17 | 18 | private readonly Dictionary _cacheHtml = new(); 19 | 20 | public TelegramApi(IUpdater updater) 21 | { 22 | _updater = updater; 23 | } 24 | 25 | public async Task SendTextMessageAsync(string userName, string text) 26 | { 27 | var chatId = new ChatId(userName); 28 | using (StringReader reader = new StringReader(text)) 29 | { 30 | StringBuilder message = new(); 31 | 32 | while (true) 33 | { 34 | var length = message.Length; 35 | var line = reader.ReadLine(); 36 | 37 | var send = line is null || length + line.Length > SettingsManager.MAX_LENGTH_MESSAGE; 38 | if (send) 39 | { 40 | var partMessage = message.ToString(); 41 | message.Clear(); 42 | if (!string.IsNullOrEmpty(partMessage)) 43 | await InvokeTelegramMethod(nameof(SendTextMessageAsync), chatId, partMessage); 44 | } 45 | 46 | if (line is null) 47 | break; 48 | else 49 | message.AppendLine(line); 50 | } 51 | } 52 | 53 | } 54 | 55 | 56 | private void DelayApi() 57 | { 58 | Thread.Sleep(SettingsManager.DelayRequestsApi); 59 | } 60 | 61 | public async Task GetChatMemberCountAsync(ChannelModel channel) 62 | { 63 | return await InvokeTelegramMethod(nameof(GetChatMemberCountAsync), channel); 64 | } 65 | 66 | public async Task GetChatAsync(ChannelModel channel) 67 | { 68 | return await InvokeTelegramMethod(nameof(GetChatAsync), channel); 69 | } 70 | 71 | private async Task InvokeTelegramMethod(string methodName, params object[] parametres) 72 | { 73 | var chatIdRevers = false; 74 | var invokeFromHTML = false; 75 | var attemptsLeft = 1; 76 | 77 | var invokeParametrs = new object[parametres.Length + 1]; 78 | FillParametrsTelegramMethod(parametres, invokeParametrs, chatIdRevers); 79 | 80 | while (attemptsLeft <= SettingsManager.MAX_NUMBER_ATTEMPTS) 81 | { 82 | DelayApi(); 83 | try 84 | { 85 | if (invokeFromHTML) 86 | { 87 | return await InvokeTelegramMethodFromURL(methodName, parametres); 88 | } else 89 | { 90 | var task = ReflectionHelper.InvokeMethod( 91 | typeof(TelegramBotClientExtensions), 92 | methodName, invokeParametrs); 93 | if (task == null) 94 | break; 95 | return await (Task)task; 96 | } 97 | } 98 | catch (Exception ex) 99 | { 100 | var isErrorNotFound = ex.Message == SettingsManager.ERROR_CHAT_NOT_FOUND; 101 | var isErrorSupergroup = ex.Message == SettingsManager.ERROR_CHAT_SUPERGROUP; 102 | 103 | //Не понял почему, но в каких-то случаях телеграм принимает username, а в каких-то id 104 | //А иногда вообще никак не находит, (например для супергруп) 105 | // - сначала ищем по стандартному ключу (username для каналов и ID для чатов) 106 | // - если не удалось, то ищем наоборот (username для чатов и ID для каналов) 107 | // - иначе же просто парсим HTML по ссылке 108 | if (isErrorNotFound) 109 | { 110 | if (chatIdRevers) 111 | invokeFromHTML = true; 112 | else 113 | { 114 | chatIdRevers = true; 115 | FillParametrsTelegramMethod(parametres, invokeParametrs, chatIdRevers); 116 | } 117 | } else if (isErrorSupergroup) { 118 | invokeFromHTML = true; 119 | } else { 120 | attemptsLeft++; 121 | 122 | _updater.AddToLog(ex.Message, parametres, LogEventType.Error); 123 | handleError(ex); 124 | } 125 | } 126 | } 127 | return default(TResult); 128 | } 129 | 130 | private void FillParametrsTelegramMethod(object[] parametrs, object[] invokeParametrs, bool chatIdRevers = false) 131 | { 132 | invokeParametrs[0] = _telegram; 133 | for (int i = 0; i < parametrs.Length; i++) 134 | { 135 | var par = parametrs[i]; 136 | if (par is ChannelModel channel) 137 | par = GetChatId(channel, chatIdRevers); 138 | invokeParametrs[i + 1] = par; 139 | } 140 | } 141 | 142 | private void handleError(Exception ex) 143 | { 144 | if (ex is ApiRequestException apiEx) 145 | { 146 | var retry = apiEx.Parameters?.RetryAfter ?? 0; 147 | if (retry > 0) 148 | { 149 | Thread.Sleep(retry * 1000); 150 | 151 | } 152 | } 153 | } 154 | 155 | public ChatId GetChatId(ChannelModel channel, bool revers = false) 156 | { 157 | var returnId = !channel.IsChannel; 158 | 159 | if (revers) 160 | returnId = !returnId; 161 | 162 | if (returnId) 163 | return new(channel.ID); 164 | else 165 | return new("@" + channel.UserName); 166 | } 167 | 168 | 169 | private async Task InvokeTelegramMethodFromURL(string methodName, params object[] parametres) 170 | { 171 | ChannelModel? channel = null; 172 | foreach (var parametr in parametres) 173 | if (parametr is ChannelModel ch) 174 | channel = ch; 175 | 176 | if (channel != null) 177 | { 178 | string? html; 179 | if (_cacheHtml.ContainsKey(channel)) 180 | html = _cacheHtml[channel]; 181 | else 182 | { 183 | using var client = new HttpClient(); 184 | html = await client.GetStringAsync(channel.Url); 185 | _cacheHtml.Add(channel, html); 186 | } 187 | 188 | if (methodName == nameof(GetChatMemberCountAsync)) 189 | return (TResult)(object)ExtractCountMembersFromURL(html); 190 | else if (methodName == nameof(GetChatAsync)) 191 | { 192 | Chat chat = new Chat(); 193 | chat.Title = ExtractNameFromURL(html); 194 | chat.Description = ExtractDescriptionFromURL(html); 195 | chat.Type = Telegram.Bot.Types.Enums.ChatType.Supergroup; 196 | return (TResult)(object)chat; 197 | } 198 | } 199 | 200 | return default(TResult); 201 | 202 | } 203 | 204 | private int ExtractCountMembersFromURL(string html) 205 | { 206 | var regex = new Regex(@"
(\d+)"); 207 | var match = regex.Match(html); 208 | 209 | if (match.Success && int.TryParse(match.Groups[1].Value, out var countMembers)) 210 | { 211 | return countMembers; 212 | } 213 | 214 | return 0; 215 | } 216 | 217 | private string ExtractNameFromURL(string html) 218 | { 219 | var regex = new Regex(@"
[\s\S]*?(.*?)"); 220 | var match = regex.Match(html); 221 | 222 | return match.Success ? match.Groups[1].Value : string.Empty; 223 | } 224 | 225 | private string ExtractDescriptionFromURL(string html) 226 | { 227 | var result = string.Empty; 228 | var regex = new Regex(@"
(.*?)
"); 229 | var match = regex.Match(html); 230 | 231 | if (match.Success) 232 | { 233 | result = match.Groups[1].Value.Replace("
", "\n"); 234 | result = RemoveHtmlLinks(result); 235 | } 236 | 237 | return result; 238 | } 239 | 240 | private string RemoveHtmlLinks(string input) 241 | { 242 | string pattern = "(.*?)"; 243 | string replacement = "$1"; 244 | string result = Regex.Replace(input, pattern, replacement); 245 | return result; 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/Tools/ArrayExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace UpdaterLibrary.Tools; 4 | public static class ArrayExtension 5 | { 6 | public static string ToStringRecurs(this object obj) 7 | { 8 | if (obj is object[] array) 9 | return string.Join(", ", array.Select(item => item?.ToStringRecurs() ?? "")); 10 | else if (obj is List list) 11 | return string.Join(", ", list.Select(item => item?.ToStringRecurs() ?? "")); 12 | else return obj.ToString() ?? ""; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/Tools/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | namespace UpdaterLibrary.Tools; 2 | 3 | public static class ReflectionHelper 4 | { 5 | public static object InvokeMethod(Type type, string methodName, object[] parameters) 6 | { 7 | // Получаем все методы с заданным именем 8 | var methods = type.GetMethods().Where(m => m.Name == methodName).ToArray(); 9 | 10 | foreach (var method in methods) 11 | { 12 | // Получаем параметры метода 13 | var methodParameters = method.GetParameters(); 14 | 15 | // Проверяем, что количество параметров метода не меньше, чем в переданном массиве 16 | if (methodParameters.Length < parameters.Length) 17 | continue; 18 | 19 | // Проверяем, совпадают ли типы первых параметров 20 | bool parametersMatch = true; 21 | for (int i = 0; i < parameters.Length; i++) 22 | { 23 | // Если типы не совпадают и не совпадают с Nullable версией типа 24 | if (!methodParameters[i].ParameterType.IsAssignableFrom(parameters[i].GetType()) && 25 | !(Nullable.GetUnderlyingType(methodParameters[i].ParameterType)?.IsAssignableFrom(parameters[i].GetType()) ?? false)) 26 | { 27 | parametersMatch = false; 28 | break; 29 | } 30 | } 31 | 32 | if (parametersMatch) 33 | { 34 | // Создаём массив параметров для вызова, заполняем его начальными значениями и Type.Missing для недостающих 35 | var invokeParameters = new object[methodParameters.Length]; 36 | for (int i = 0; i < invokeParameters.Length; i++) 37 | { 38 | if (i < parameters.Length) 39 | { 40 | invokeParameters[i] = parameters[i]; 41 | } 42 | else 43 | { 44 | invokeParameters[i] = Type.Missing; 45 | } 46 | } 47 | 48 | // Вызываем метод 49 | return method.Invoke(null, invokeParameters); 50 | } 51 | } 52 | 53 | throw new InvalidOperationException("No suitable method found."); 54 | } 55 | } -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/UpdateHandlers/ChannelInfoUpdateHandler.cs: -------------------------------------------------------------------------------- 1 | using UpdaterLibrary.SiteUpdater; 2 | using Telegram.Bot.Types.Enums; 3 | using UpdaterLibrary.Settings; 4 | 5 | namespace UpdaterLibrary.UpdateHandlers; 6 | 7 | internal class ChannelInfoUpdateHandler : IUpdateHandler 8 | { 9 | public async Task UpdateAsync(IUpdater updater) 10 | { 11 | foreach (var channel in updater.Data.Channels) 12 | { 13 | if (!updater.ChannelUpdateRequired(channel)) 14 | continue; 15 | 16 | var info = await updater.GetChatAsync(channel); 17 | if (info is null) 18 | continue; 19 | 20 | bool modified = false; 21 | 22 | var hasFirstOrLastName = info.FirstName is not null || info.LastName is not null; 23 | 24 | if (info.Id != 0 && info.Id != channel.ID) 25 | { 26 | channel.ID = info.Id; 27 | modified = true; 28 | } 29 | 30 | var usename = info.Username ?? ""; 31 | if (!String.IsNullOrEmpty(usename) && usename != channel.UserName) 32 | { 33 | channel.UserName = usename; 34 | modified = true; 35 | } 36 | 37 | var description = info.Description ?? ""; 38 | if (description != channel.Description) 39 | { 40 | channel.Description = description; 41 | modified = true; 42 | } 43 | 44 | string title; 45 | if (info.Title is null && hasFirstOrLastName) 46 | title = $"{info.FirstName} {info.LastName ?? ""}"; 47 | else 48 | title = info.Title ?? ""; 49 | 50 | if (title != channel.Name) 51 | { 52 | channel.Name = title; 53 | modified = true; 54 | } 55 | 56 | var isChannel = info.Type == ChatType.Channel; 57 | if (channel.IsChannel != isChannel) 58 | { 59 | channel.IsChannel = isChannel; 60 | modified = true; 61 | } 62 | 63 | if (modified) 64 | { 65 | updater.AddToLog("Актуализирована информация о канале", channel); 66 | } 67 | 68 | var isBot = !isChannel 69 | && channel.UserName.EndsWith("bot", true, null) 70 | && info.Type == ChatType.Private 71 | && !hasFirstOrLastName; 72 | 73 | var isUser = !isChannel && hasFirstOrLastName; 74 | var isChat = !isChannel && !isBot && !isUser; 75 | 76 | updater.UpdateChannelTag(channel, SettingsManager.TAG_NAME_CHANNEL, isChannel); 77 | updater.UpdateChannelTag(channel, SettingsManager.TAG_NAME_CHAT , isChat); 78 | updater.UpdateChannelTag(channel, SettingsManager.TAG_NAME_BOT , isBot); 79 | updater.UpdateChannelTag(channel, SettingsManager.TAG_NAME_USER , isUser); 80 | 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/UpdateHandlers/IUpdateHandler.cs: -------------------------------------------------------------------------------- 1 | using UpdaterLibrary.SiteUpdater; 2 | 3 | namespace UpdaterLibrary.UpdateHandlers; 4 | 5 | public interface IUpdateHandler 6 | { 7 | public Task UpdateAsync(IUpdater updater); 8 | } 9 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/UpdateHandlers/IrrelevantTagsUpdateHandler.cs: -------------------------------------------------------------------------------- 1 | using UpdaterLibrary.SiteUpdater; 2 | 3 | namespace UpdaterLibrary.UpdateHandlers; 4 | 5 | public class IrrelevantTagsUpdateHandler : IUpdateHandler 6 | { 7 | public Task UpdateAsync(IUpdater updater) 8 | { 9 | var data = updater.Data; 10 | 11 | foreach (var channel in data.Channels) 12 | { 13 | if (!updater.ChannelUpdateRequired(channel)) 14 | continue; 15 | 16 | var irrelevantTags = channel.Tags.Where( 17 | tagChannel => !data.Tags.Exists(t => t.Name == tagChannel) 18 | ).ToArray(); 19 | 20 | foreach (var tag in irrelevantTags) 21 | { 22 | updater.UpdateChannelTag(channel, tag, false); 23 | } 24 | 25 | } 26 | 27 | return Task.CompletedTask; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/UpdateHandlers/MemberCountUpdateHandler.cs: -------------------------------------------------------------------------------- 1 | using UpdaterLibrary.Settings; 2 | using UpdaterLibrary.SiteUpdater; 3 | 4 | namespace UpdaterLibrary.UpdateHandlers; 5 | 6 | internal class MemberCountUpdateHandler : IUpdateHandler 7 | { 8 | const int MEMBER_COUNT_TAG_BEGIN = 500; 9 | 10 | public async Task UpdateAsync(IUpdater updater) 11 | { 12 | foreach (var channel in updater.Data.Channels) 13 | { 14 | if (!updater.ChannelUpdateRequired(channel)) 15 | continue; 16 | 17 | var count = await updater.GetMemberCountAsync(channel); 18 | if (count is null) 19 | continue; 20 | 21 | //TODO: вынести типы, которые сейчас в тегах. 22 | //Сейчас считаем, что если 2 участника чата, то это чат с ботом или пользователем 23 | // и для него не нужно ставить тег "Малыши" 24 | updater.UpdateChannelTag(channel, 25 | SettingsManager.TAG_NAME_BEGIN, 26 | count <= MEMBER_COUNT_TAG_BEGIN && count > 2); 27 | 28 | count = count / 1000; 29 | if (count != channel.MemberCount) 30 | { 31 | channel.MemberCount = (int)count; 32 | updater.AddToLog("Актуализировано число подписчиков", channel); 33 | } 34 | 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/UpdateHandlers/TempUpdateHandler.cs: -------------------------------------------------------------------------------- 1 | using UpdaterLibrary.Settings; 2 | using UpdaterLibrary.SiteUpdater; 3 | 4 | namespace UpdaterLibrary.UpdateHandlers; 5 | public class TempUpdateHandler : IUpdateHandler 6 | { 7 | public Task UpdateAsync(IUpdater updater) 8 | { 9 | 10 | foreach (var channel in updater.Data.Channels) 11 | { 12 | channel.IsChannel = channel.Tags.Contains(SettingsManager.TAG_NAME_CHANNEL); 13 | } 14 | 15 | return Task.CompletedTask; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/Updater.cs: -------------------------------------------------------------------------------- 1 | using UpdaterLibrary.Models; 2 | using UpdaterLibrary.Settings; 3 | using System.Text.Json; 4 | using UpdaterLibrary.UpdateHandlers; 5 | using System.Text; 6 | using UpdaterLibrary.SiteUpdater; 7 | using Telegram.Bot.Types; 8 | using System.Text.Encodings.Web; 9 | using UpdaterLibrary.TelegramBot; 10 | using UpdaterLibrary.Tools; 11 | 12 | namespace UpdaterLibrary; 13 | 14 | public class Updater : IUpdater 15 | { 16 | public UpdateDataModel Data { get; set; } = null!; 17 | public UpdateParametrs Parametrs { get; set; } = new(); 18 | public Dictionary Log { get; set; } = new(); 19 | 20 | private DateTime _dateStartUpdate; 21 | 22 | private readonly TelegramApi _telegram; 23 | 24 | private readonly List> logHandlers = new(); 25 | 26 | private readonly List _handlers = [ 27 | new IrrelevantTagsUpdateHandler(), 28 | new ChannelInfoUpdateHandler(), 29 | new MemberCountUpdateHandler(), 30 | //new TempUpdateHandler(), 31 | ]; 32 | 33 | public Updater() 34 | { 35 | _telegram = new(this); 36 | } 37 | 38 | public async Task UpdateAsync() 39 | { 40 | try 41 | { 42 | _dateStartUpdate = DateTime.Now; 43 | 44 | ReadSiteData(); 45 | 46 | foreach (var handler in _handlers) 47 | { 48 | await handler.UpdateAsync(this); 49 | } 50 | 51 | await SaveChangesAsync(); 52 | 53 | await NotifySubscribers(); 54 | } 55 | catch (Exception ex) 56 | { 57 | await NotifyExceptionSubscribers(ex); 58 | throw; 59 | } 60 | 61 | } 62 | 63 | public void ReadSiteData() 64 | { 65 | var filePath = SettingsManager.PathToFileChannels; 66 | 67 | if (!System.IO.File.Exists(filePath)) 68 | { 69 | throw new FileNotFoundException($"Не найден файл каналов: {filePath}"); 70 | } 71 | 72 | using (var reader = new StreamReader(filePath)) 73 | { 74 | var fileContents = reader.ReadToEnd(); 75 | 76 | var options = new JsonSerializerOptions { 77 | PropertyNameCaseInsensitive = true, 78 | }; 79 | var result = JsonSerializer.Deserialize(fileContents, options); 80 | 81 | if (result is null) 82 | throw new FileLoadException($"Файл каналов пустой: {filePath}"); 83 | else if (result.Channels is null) 84 | throw new FileLoadException($"Файл не содержит каналы: {filePath}"); 85 | else if (result.Tags is null) 86 | throw new FileLoadException($"Файл не содержит теги: {filePath}"); 87 | else 88 | Data = result; 89 | } 90 | } 91 | 92 | public void UpdateChannelTag(ChannelModel channel, string TagName, bool Add) 93 | { 94 | var contains = channel.Tags.Contains(TagName); 95 | if (Add && !contains) 96 | { 97 | channel.Tags.Add(TagName); 98 | AddToLog($"Добавлен тег '{TagName}'", channel); 99 | } 100 | else if (!Add && contains) 101 | { 102 | channel.Tags.Remove(TagName); 103 | AddToLog($"Удален тег '{TagName}'", channel); 104 | } 105 | } 106 | 107 | public void AddLogHandler(Action handler) 108 | { 109 | logHandlers.Add(handler); 110 | } 111 | 112 | public void AddToLog(string message, object obj, LogEventType type = LogEventType.Change) 113 | { 114 | LogEventModel logEvent; 115 | 116 | if (Log.ContainsKey(message)) 117 | { 118 | logEvent = Log[message]; 119 | } 120 | else 121 | { 122 | logEvent = new(); 123 | logEvent.Text = message; 124 | logEvent.Type = type; 125 | Log.Add(message, logEvent); 126 | } 127 | 128 | if (!logEvent.Objects.Contains(obj)) 129 | { 130 | logEvent.Objects.Add(obj); 131 | } 132 | 133 | if (type == LogEventType.Change && obj is ChannelModel) 134 | { 135 | ((ChannelModel)obj).Actual = DateTime.Now; 136 | } 137 | 138 | foreach (var logHandler in logHandlers) 139 | { 140 | logHandler.Invoke(message, obj, type); 141 | } 142 | 143 | } 144 | 145 | public string LogText(LogEventType? filterType = null) 146 | { 147 | var builder = new StringBuilder(); 148 | 149 | foreach (var item in Log) 150 | { 151 | var logEvent = item.Value; 152 | 153 | if (filterType != null && logEvent.Type != filterType) 154 | continue; 155 | 156 | List listStr = new(); 157 | 158 | builder.Append($"{item.Key}: "); 159 | 160 | foreach (var itemObj in logEvent.Objects) 161 | { 162 | var thisStr = itemObj.ToStringRecurs(); 163 | if (!listStr.Contains(thisStr)) 164 | listStr.Add(thisStr); 165 | } 166 | 167 | builder.Append(listStr.ToStringRecurs()); 168 | builder.AppendLine(); 169 | } 170 | 171 | return builder.ToString(); 172 | } 173 | 174 | public bool ChannelUpdateRequired(ChannelModel channel) 175 | { 176 | if (Parametrs.ChannelID != 0) 177 | return channel.ID == Parametrs.ChannelID; 178 | else if (!string.IsNullOrEmpty(Parametrs.ChannelUserName)) 179 | return string.Equals(channel.UserName, Parametrs.ChannelUserName, StringComparison.OrdinalIgnoreCase); 180 | else 181 | return !channel.Tags.Contains(SettingsManager.TAG_NAME_CLOSE); 182 | } 183 | 184 | private async Task NotifySubscribers() 185 | { 186 | var dateFinishUpdate = DateTime.Now; 187 | 188 | var errors = LogText(LogEventType.Error); 189 | var changes = LogText(LogEventType.Change); 190 | 191 | foreach (var subscriber in SettingsManager.Subscribers) 192 | { 193 | await _telegram.SendTextMessageAsync(subscriber, $"Начало: {_dateStartUpdate}, Конец: {dateFinishUpdate}"); 194 | 195 | if (!string.IsNullOrEmpty(errors)) 196 | { 197 | errors = $"ОШИБКИ!!! \n{errors}"; 198 | await _telegram.SendTextMessageAsync(subscriber, errors); 199 | } 200 | 201 | if (!string.IsNullOrEmpty(changes)) 202 | { 203 | changes = $"Изменения: \n{changes}"; 204 | await _telegram.SendTextMessageAsync(subscriber, changes); 205 | } 206 | } 207 | } 208 | 209 | private async Task NotifyExceptionSubscribers(Exception ex) 210 | { 211 | foreach (var subscriber in SettingsManager.Subscribers) 212 | { 213 | await _telegram.SendTextMessageAsync(subscriber, ex.Message); 214 | } 215 | } 216 | private async Task SaveChangesAsync() 217 | { 218 | if (!Parametrs.SaveChanges) 219 | return; 220 | 221 | var options = new JsonSerializerOptions { 222 | WriteIndented = true, 223 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping 224 | }; 225 | 226 | string jsonString = JsonSerializer.Serialize(Data, options); 227 | 228 | await System.IO.File.WriteAllTextAsync(SettingsManager.PathToFileChannels, jsonString); 229 | 230 | } 231 | 232 | public Task GetMemberCountAsync(ChannelModel channel) 233 | { 234 | return _telegram.GetChatMemberCountAsync(channel); 235 | } 236 | 237 | public Task GetChatAsync(ChannelModel channel) 238 | { 239 | return _telegram.GetChatAsync(channel); 240 | } 241 | 242 | } 243 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/UpdaterLibrary.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tools/updater/UpdaterLibrary/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "PathToSite": "Локальный путь к папке с сайтом проекта", 3 | "TelegramBotToken": "Токен доступа к боту Телеграм", 4 | "DelayRequestsApi": 3000, 5 | "Subscribers": [ 6 | 221535085 7 | ] 8 | } --------------------------------------------------------------------------------