├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── examples ├── list.py └── post.py ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── slacker ├── __init__.py └── utilities.py ├── static └── slacker.jpg ├── tests ├── __init__.py ├── test_channels.py ├── test_utilities.py └── utilities.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS 2 | .DS_Store 3 | 4 | # Python 5 | *.pyc 6 | *.pyo 7 | dist 8 | slacker.egg-info 9 | .tox 10 | ### JetBrains template 11 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 12 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 13 | 14 | # User-specific stuff: 15 | .idea/**/workspace.xml 16 | .idea/**/tasks.xml 17 | .idea/dictionaries 18 | 19 | # Sensitive or high-churn files: 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.xml 23 | .idea/**/dataSources.local.xml 24 | .idea/**/sqlDataSources.xml 25 | .idea/**/dynamic.xml 26 | .idea/**/uiDesigner.xml 27 | 28 | # Gradle: 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Mongo Explorer plugin: 33 | .idea/**/mongoSettings.xml 34 | 35 | ## File-based project format: 36 | *.iws 37 | 38 | ## Plugin-specific files: 39 | 40 | # IntelliJ 41 | /out/ 42 | 43 | # mpeltonen/sbt-idea plugin 44 | .idea_modules/ 45 | 46 | # JIRA plugin 47 | atlassian-ide-plugin.xml 48 | 49 | # Crashlytics plugin (for Android Studio and IntelliJ) 50 | com_crashlytics_export_strings.xml 51 | crashlytics.properties 52 | crashlytics-build.properties 53 | fabric.properties 54 | ### Linux template 55 | *~ 56 | 57 | # temporary files which can be created if a process still has a handle open of a deleted file 58 | .fuse_hidden* 59 | 60 | # KDE directory preferences 61 | .directory 62 | 63 | # Linux trash folder which might appear on any partition or disk 64 | .Trash-* 65 | 66 | # .nfs files are created when an open file is removed but is still being accessed 67 | .nfs* 68 | ### VisualStudio template 69 | ## Ignore Visual Studio temporary files, build results, and 70 | ## files generated by popular Visual Studio add-ons. 71 | ## 72 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 73 | 74 | # User-specific files 75 | *.suo 76 | *.user 77 | *.userosscache 78 | *.sln.docstates 79 | 80 | # User-specific files (MonoDevelop/Xamarin Studio) 81 | *.userprefs 82 | 83 | # Build results 84 | [Dd]ebug/ 85 | [Dd]ebugPublic/ 86 | [Rr]elease/ 87 | [Rr]eleases/ 88 | x64/ 89 | x86/ 90 | bld/ 91 | [Bb]in/ 92 | [Oo]bj/ 93 | [Ll]og/ 94 | 95 | # Visual Studio 2015 cache/options directory 96 | .vs/ 97 | # Uncomment if you have tasks that create the project's static files in wwwroot 98 | #wwwroot/ 99 | 100 | # MSTest test Results 101 | [Tt]est[Rr]esult*/ 102 | [Bb]uild[Ll]og.* 103 | 104 | # NUNIT 105 | *.VisualState.xml 106 | TestResult.xml 107 | 108 | # Build Results of an ATL Project 109 | [Dd]ebugPS/ 110 | [Rr]eleasePS/ 111 | dlldata.c 112 | 113 | # .NET Core 114 | project.lock.json 115 | project.fragment.lock.json 116 | artifacts/ 117 | **/Properties/launchSettings.json 118 | 119 | *_i.c 120 | *_p.c 121 | *_i.h 122 | *.ilk 123 | *.meta 124 | *.obj 125 | *.pch 126 | *.pdb 127 | *.pgc 128 | *.pgd 129 | *.rsp 130 | *.sbr 131 | *.tlb 132 | *.tli 133 | *.tlh 134 | *.tmp 135 | *.tmp_proj 136 | *.log 137 | *.vspscc 138 | *.vssscc 139 | .builds 140 | *.pidb 141 | *.svclog 142 | *.scc 143 | 144 | # Chutzpah Test files 145 | _Chutzpah* 146 | 147 | # Visual C++ cache files 148 | ipch/ 149 | *.aps 150 | *.ncb 151 | *.opendb 152 | *.opensdf 153 | *.sdf 154 | *.cachefile 155 | *.VC.db 156 | *.VC.VC.opendb 157 | 158 | # Visual Studio profiler 159 | *.psess 160 | *.vsp 161 | *.vspx 162 | *.sap 163 | 164 | # TFS 2012 Local Workspace 165 | $tf/ 166 | 167 | # Guidance Automation Toolkit 168 | *.gpState 169 | 170 | # ReSharper is a .NET coding add-in 171 | _ReSharper*/ 172 | *.[Rr]e[Ss]harper 173 | *.DotSettings.user 174 | 175 | # JustCode is a .NET coding add-in 176 | .JustCode 177 | 178 | # TeamCity is a build add-in 179 | _TeamCity* 180 | 181 | # DotCover is a Code Coverage Tool 182 | *.dotCover 183 | 184 | # Visual Studio code coverage results 185 | *.coverage 186 | *.coveragexml 187 | 188 | # NCrunch 189 | _NCrunch_* 190 | .*crunch*.local.xml 191 | nCrunchTemp_* 192 | 193 | # MightyMoose 194 | *.mm.* 195 | AutoTest.Net/ 196 | 197 | # Web workbench (sass) 198 | .sass-cache/ 199 | 200 | # Installshield output folder 201 | [Ee]xpress/ 202 | 203 | # DocProject is a documentation generator add-in 204 | DocProject/buildhelp/ 205 | DocProject/Help/*.HxT 206 | DocProject/Help/*.HxC 207 | DocProject/Help/*.hhc 208 | DocProject/Help/*.hhk 209 | DocProject/Help/*.hhp 210 | DocProject/Help/Html2 211 | DocProject/Help/html 212 | 213 | # Click-Once directory 214 | publish/ 215 | 216 | # Publish Web Output 217 | *.[Pp]ublish.xml 218 | *.azurePubxml 219 | # TODO: Comment the next line if you want to checkin your web deploy settings 220 | # but database connection strings (with potential passwords) will be unencrypted 221 | *.pubxml 222 | *.publishproj 223 | 224 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 225 | # checkin your Azure Web App publish settings, but sensitive information contained 226 | # in these scripts will be unencrypted 227 | PublishScripts/ 228 | 229 | # NuGet Packages 230 | *.nupkg 231 | # The packages folder can be ignored because of Package Restore 232 | **/packages/* 233 | # except build/, which is used as an MSBuild target. 234 | !**/packages/build/ 235 | # Uncomment if necessary however generally it will be regenerated when needed 236 | #!**/packages/repositories.config 237 | # NuGet v3's project.json files produces more ignorable files 238 | *.nuget.props 239 | *.nuget.targets 240 | 241 | # Microsoft Azure Build Output 242 | csx/ 243 | *.build.csdef 244 | 245 | # Microsoft Azure Emulator 246 | ecf/ 247 | rcf/ 248 | 249 | # Windows Store app package directories and files 250 | AppPackages/ 251 | BundleArtifacts/ 252 | Package.StoreAssociation.xml 253 | _pkginfo.txt 254 | 255 | # Visual Studio cache files 256 | # files ending in .cache can be ignored 257 | *.[Cc]ache 258 | # but keep track of directories ending in .cache 259 | !*.[Cc]ache/ 260 | 261 | # Others 262 | ClientBin/ 263 | ~$* 264 | *~ 265 | *.dbmdl 266 | *.dbproj.schemaview 267 | *.jfm 268 | *.pfx 269 | *.publishsettings 270 | orleans.codegen.cs 271 | 272 | # Since there are multiple workflows, uncomment next line to ignore bower_components 273 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 274 | #bower_components/ 275 | 276 | # RIA/Silverlight projects 277 | Generated_Code/ 278 | 279 | # Backup & report files from converting an old project file 280 | # to a newer Visual Studio version. Backup files are not needed, 281 | # because we have git ;-) 282 | _UpgradeReport_Files/ 283 | Backup*/ 284 | UpgradeLog*.XML 285 | UpgradeLog*.htm 286 | 287 | # SQL Server files 288 | *.mdf 289 | *.ldf 290 | 291 | # Business Intelligence projects 292 | *.rdl.data 293 | *.bim.layout 294 | *.bim_*.settings 295 | 296 | # Microsoft Fakes 297 | FakesAssemblies/ 298 | 299 | # GhostDoc plugin setting file 300 | *.GhostDoc.xml 301 | 302 | # Node.js Tools for Visual Studio 303 | .ntvs_analysis.dat 304 | node_modules/ 305 | 306 | # Typescript v1 declaration files 307 | typings/ 308 | 309 | # Visual Studio 6 build log 310 | *.plg 311 | 312 | # Visual Studio 6 workspace options file 313 | *.opt 314 | 315 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 316 | *.vbw 317 | 318 | # Visual Studio LightSwitch build output 319 | **/*.HTMLClient/GeneratedArtifacts 320 | **/*.DesktopClient/GeneratedArtifacts 321 | **/*.DesktopClient/ModelManifest.xml 322 | **/*.Server/GeneratedArtifacts 323 | **/*.Server/ModelManifest.xml 324 | _Pvt_Extensions 325 | 326 | # Paket dependency manager 327 | .paket/paket.exe 328 | paket-files/ 329 | 330 | # FAKE - F# Make 331 | .fake/ 332 | 333 | # JetBrains Rider 334 | .idea/ 335 | *.sln.iml 336 | 337 | # CodeRush 338 | .cr/ 339 | 340 | # Python Tools for Visual Studio (PTVS) 341 | __pycache__/ 342 | *.pyc 343 | 344 | # Cake - Uncomment if you are using it 345 | # tools/** 346 | # !tools/packages.config 347 | ### VirtualEnv template 348 | # Virtualenv 349 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 350 | .Python 351 | [Bb]in 352 | [Ii]nclude 353 | [Ll]ib 354 | [Ll]ib64 355 | [Ll]ocal 356 | [Ss]cripts 357 | pyvenv.cfg 358 | .venv 359 | pip-selfcheck.json 360 | ### Windows template 361 | # Windows thumbnail cache files 362 | Thumbs.db 363 | ehthumbs.db 364 | ehthumbs_vista.db 365 | 366 | # Folder config file 367 | Desktop.ini 368 | 369 | # Recycle Bin used on file shares 370 | $RECYCLE.BIN/ 371 | 372 | # Windows Installer files 373 | *.cab 374 | *.msi 375 | *.msm 376 | *.msp 377 | 378 | # Windows shortcuts 379 | *.lnk 380 | ### macOS template 381 | *.DS_Store 382 | .AppleDouble 383 | .LSOverride 384 | 385 | # Icon must end with two \r 386 | Icon 387 | 388 | 389 | # Thumbnails 390 | ._* 391 | 392 | # Files that might appear in the root of a volume 393 | .DocumentRevisions-V100 394 | .fseventsd 395 | .Spotlight-V100 396 | .TemporaryItems 397 | .Trashes 398 | .VolumeIcon.icns 399 | .com.apple.timemachine.donotpresent 400 | 401 | # Directories potentially created on remote AFP share 402 | .AppleDB 403 | .AppleDesktop 404 | Network Trash Folder 405 | Temporary Items 406 | .apdisk 407 | ### Xcode template 408 | # Xcode 409 | # 410 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 411 | 412 | ## Build generated 413 | build/ 414 | DerivedData/ 415 | 416 | ## Various settings 417 | *.pbxuser 418 | !default.pbxuser 419 | *.mode1v3 420 | !default.mode1v3 421 | *.mode2v3 422 | !default.mode2v3 423 | *.perspectivev3 424 | !default.perspectivev3 425 | xcuserdata/ 426 | 427 | ## Other 428 | *.moved-aside 429 | *.xccheckout 430 | *.xcscmblueprint 431 | ### Vim template 432 | # swap 433 | [._]*.s[a-v][a-z] 434 | [._]*.sw[a-p] 435 | [._]s[a-v][a-z] 436 | [._]sw[a-p] 437 | # session 438 | Session.vim 439 | # temporary 440 | .netrwhist 441 | *~ 442 | # auto-generated tag files 443 | tags 444 | ### Eclipse template 445 | 446 | .metadata 447 | bin/ 448 | tmp/ 449 | *.tmp 450 | *.bak 451 | *.swp 452 | *~.nib 453 | local.properties 454 | .settings/ 455 | .loadpath 456 | .recommenders 457 | 458 | # Eclipse Core 459 | .project 460 | 461 | # External tool builders 462 | .externalToolBuilders/ 463 | 464 | # Locally stored "Eclipse launch configurations" 465 | *.launch 466 | 467 | # PyDev specific (Python IDE for Eclipse) 468 | *.pydevproject 469 | 470 | # CDT-specific (C/C++ Development Tooling) 471 | .cproject 472 | 473 | # JDT-specific (Eclipse Java Development Tools) 474 | .classpath 475 | 476 | # Java annotation processor (APT) 477 | .factorypath 478 | 479 | # PDT-specific (PHP Development Tools) 480 | .buildpath 481 | 482 | # sbteclipse plugin 483 | .target 484 | 485 | # Tern plugin 486 | .tern-project 487 | 488 | # TeXlipse plugin 489 | .texlipse 490 | 491 | # STS (Spring Tool Suite) 492 | .springBeans 493 | 494 | # Code Recommenders 495 | .recommenders/ 496 | 497 | # Scala IDE specific (Scala & Java development for Eclipse) 498 | .cache-main 499 | .scala_dependencies 500 | .worksheet 501 | ### Emacs template 502 | # -*- mode: gitignore; -*- 503 | *~ 504 | \#*\# 505 | /.emacs.desktop 506 | /.emacs.desktop.lock 507 | *.elc 508 | auto-save-list 509 | tramp 510 | .\#* 511 | 512 | # Org-mode 513 | .org-id-locations 514 | *_archive 515 | 516 | # flymake-mode 517 | *_flymake.* 518 | 519 | # eshell files 520 | /eshell/history 521 | /eshell/lastdir 522 | 523 | # elpa packages 524 | /elpa/ 525 | 526 | # reftex files 527 | *.rel 528 | 529 | # AUCTeX auto folder 530 | /auto/ 531 | 532 | # cask packages 533 | .cask/ 534 | dist/ 535 | 536 | # Flycheck 537 | flycheck_*.el 538 | 539 | # server auth directory 540 | /server/ 541 | 542 | # projectiles files 543 | .projectile 544 | 545 | # directory configuration 546 | .dir-locals.el 547 | ### SublimeText template 548 | # cache files for sublime text 549 | *.tmlanguage.cache 550 | *.tmPreferences.cache 551 | *.stTheme.cache 552 | 553 | # workspace files are user-specific 554 | *.sublime-workspace 555 | 556 | # project files should be checked into the repository, unless a significant 557 | # proportion of contributors will probably not be using SublimeText 558 | # *.sublime-project 559 | 560 | # sftp configuration file 561 | sftp-config.json 562 | 563 | # Package control specific files 564 | Package Control.last-run 565 | Package Control.ca-list 566 | Package Control.ca-bundle 567 | Package Control.system-ca-bundle 568 | Package Control.cache/ 569 | Package Control.ca-certs/ 570 | Package Control.merged-ca-bundle 571 | Package Control.user-ca-bundle 572 | oscrypto-ca-bundle.crt 573 | bh_unicode_properties.cache 574 | 575 | # Sublime-github package stores a github token in this file 576 | # https://packagecontrol.io/packages/sublime-github 577 | GitHub.sublime-settings 578 | ### Python template 579 | # Byte-compiled / optimized / DLL files 580 | __pycache__/ 581 | *.py[cod] 582 | *$py.class 583 | 584 | # C extensions 585 | *.so 586 | 587 | # Distribution / packaging 588 | .Python 589 | env/ 590 | build/ 591 | develop-eggs/ 592 | dist/ 593 | downloads/ 594 | eggs/ 595 | .eggs/ 596 | lib/ 597 | lib64/ 598 | parts/ 599 | sdist/ 600 | var/ 601 | wheels/ 602 | *.egg-info/ 603 | .installed.cfg 604 | *.egg 605 | 606 | # PyInstaller 607 | # Usually these files are written by a python script from a template 608 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 609 | *.manifest 610 | *.spec 611 | 612 | # Installer logs 613 | pip-log.txt 614 | pip-delete-this-directory.txt 615 | 616 | # Unit test / coverage reports 617 | htmlcov/ 618 | .tox/ 619 | .coverage 620 | .coverage.* 621 | .cache 622 | nosetests.xml 623 | coverage.xml 624 | *,cover 625 | .hypothesis/ 626 | 627 | # Translations 628 | *.mo 629 | *.pot 630 | 631 | # Django stuff: 632 | *.log 633 | local_settings.py 634 | 635 | # Flask stuff: 636 | instance/ 637 | .webassets-cache 638 | 639 | # Scrapy stuff: 640 | .scrapy 641 | 642 | # Sphinx documentation 643 | docs/_build/ 644 | 645 | # PyBuilder 646 | target/ 647 | 648 | # Jupyter Notebook 649 | .ipynb_checkpoints 650 | 651 | # pyenv 652 | .python-version 653 | 654 | # celery beat schedule file 655 | celerybeat-schedule 656 | 657 | # SageMath parsed files 658 | *.sage.py 659 | 660 | # dotenv 661 | .env 662 | 663 | # virtualenv 664 | .venv 665 | venv/ 666 | ENV/ 667 | 668 | # Spyder project settings 669 | .spyderproject 670 | 671 | # Rope project settings 672 | .ropeproject 673 | 674 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | dist: xenial 4 | 5 | python: 6 | - "2.7" 7 | - "3.6" 8 | - "3.7" 9 | - "3.8-dev" 10 | - "pypy3.5" 11 | 12 | sudo: false 13 | 14 | install: 15 | - pip install -r requirements-dev.txt 16 | 17 | script: 18 | - pytest 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the 13 | copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other 16 | entities that control, are controlled by, or are under common control with 17 | that entity. For the purposes of this definition, "control" means (i) the 18 | power, direct or indirect, to cause the direction or management of such 19 | entity, whether by contract or otherwise, or (ii) ownership of 20 | fifty percent (50%) or more of the outstanding shares, or (iii) beneficial 21 | ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity exercising 24 | permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation source, 28 | and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical transformation 31 | or translation of a Source form, including but not limited to compiled 32 | object code, generated documentation, and conversions to 33 | other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or Object 36 | form, made available under the License, as indicated by a copyright notice 37 | that is included in or attached to the work (an example is provided in the 38 | Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object form, 41 | that is based on (or derived from) the Work and for which the editorial 42 | revisions, annotations, elaborations, or other modifications represent, 43 | as a whole, an original work of authorship. For the purposes of this 44 | License, Derivative Works shall not include works that remain separable 45 | from, or merely link (or bind by name) to the interfaces of, the Work and 46 | Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including the original 49 | version of the Work and any modifications or additions to that Work or 50 | Derivative Works thereof, that is intentionally submitted to Licensor for 51 | inclusion in the Work by the copyright owner or by an individual or 52 | Legal Entity authorized to submit on behalf of the copyright owner. 53 | For the purposes of this definition, "submitted" means any form of 54 | electronic, verbal, or written communication sent to the Licensor or its 55 | representatives, including but not limited to communication on electronic 56 | mailing lists, source code control systems, and issue tracking systems 57 | that are managed by, or on behalf of, the Licensor for the purpose of 58 | discussing and improving the Work, but excluding communication that is 59 | conspicuously marked or otherwise designated in writing by the copyright 60 | owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity on 63 | behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. 67 | 68 | Subject to the terms and conditions of this License, each Contributor 69 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 70 | royalty-free, irrevocable copyright license to reproduce, prepare 71 | Derivative Works of, publicly display, publicly perform, sublicense, 72 | and distribute the Work and such Derivative Works in 73 | Source or Object form. 74 | 75 | 3. Grant of Patent License. 76 | 77 | Subject to the terms and conditions of this License, each Contributor 78 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 79 | royalty-free, irrevocable (except as stated in this section) patent 80 | license to make, have made, use, offer to sell, sell, import, and 81 | otherwise transfer the Work, where such license applies only to those 82 | patent claims licensable by such Contributor that are necessarily 83 | infringed by their Contribution(s) alone or by combination of their 84 | Contribution(s) with the Work to which such Contribution(s) was submitted. 85 | If You institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 87 | Contribution incorporated within the Work constitutes direct or 88 | contributory patent infringement, then any patent licenses granted to 89 | You under this License for that Work shall terminate as of the date such 90 | litigation is filed. 91 | 92 | 4. Redistribution. 93 | 94 | You may reproduce and distribute copies of the Work or Derivative Works 95 | thereof in any medium, with or without modifications, and in Source or 96 | Object form, provided that You meet the following conditions: 97 | 98 | 1. You must give any other recipients of the Work or Derivative Works a 99 | copy of this License; and 100 | 101 | 2. You must cause any modified files to carry prominent notices stating 102 | that You changed the files; and 103 | 104 | 3. You must retain, in the Source form of any Derivative Works that You 105 | distribute, all copyright, patent, trademark, and attribution notices from 106 | the Source form of the Work, excluding those notices that do not pertain 107 | to any part of the Derivative Works; and 108 | 109 | 4. If the Work includes a "NOTICE" text file as part of its distribution, 110 | then any Derivative Works that You distribute must include a readable copy 111 | of the attribution notices contained within such NOTICE file, excluding 112 | those notices that do not pertain to any part of the Derivative Works, 113 | in at least one of the following places: within a NOTICE text file 114 | distributed as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, within a 116 | display generated by the Derivative Works, if and wherever such 117 | third-party notices normally appear. The contents of the NOTICE file are 118 | for informational purposes only and do not modify the License. 119 | You may add Your own attribution notices within Derivative Works that You 120 | distribute, alongside or as an addendum to the NOTICE text from the Work, 121 | provided that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and may 125 | provide additional or different license terms and conditions for use, 126 | reproduction, or distribution of Your modifications, or for any such 127 | Derivative Works as a whole, provided Your use, reproduction, and 128 | distribution of the Work otherwise complies with the conditions 129 | stated in this License. 130 | 131 | 5. Submission of Contributions. 132 | 133 | Unless You explicitly state otherwise, any Contribution intentionally 134 | submitted for inclusion in the Work by You to the Licensor shall be under 135 | the terms and conditions of this License, without any additional 136 | terms or conditions. Notwithstanding the above, nothing herein shall 137 | supersede or modify the terms of any separate license agreement you may 138 | have executed with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. 141 | 142 | This License does not grant permission to use the trade names, trademarks, 143 | service marks, or product names of the Licensor, except as required for 144 | reasonable and customary use in describing the origin of the Work and 145 | reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. 148 | 149 | Unless required by applicable law or agreed to in writing, Licensor 150 | provides the Work (and each Contributor provides its Contributions) 151 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 152 | either express or implied, including, without limitation, any warranties 153 | or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS 154 | FOR A PARTICULAR PURPOSE. You are solely responsible for determining the 155 | appropriateness of using or redistributing the Work and assume any risks 156 | associated with Your exercise of permissions under this License. 157 | 158 | 8. Limitation of Liability. 159 | 160 | In no event and under no legal theory, whether in tort 161 | (including negligence), contract, or otherwise, unless required by 162 | applicable law (such as deliberate and grossly negligent acts) or agreed 163 | to in writing, shall any Contributor be liable to You for damages, 164 | including any direct, indirect, special, incidental, or consequential 165 | damages of any character arising as a result of this License or out of 166 | the use or inability to use the Work (including but not limited to damages 167 | for loss of goodwill, work stoppage, computer failure or malfunction, 168 | or any and all other commercial damages or losses), even if such 169 | Contributor has been advised of the possibility of such damages. 170 | 171 | 9. Accepting Warranty or Additional Liability. 172 | 173 | While redistributing the Work or Derivative Works thereof, You may choose 174 | to offer, and charge a fee for, acceptance of support, warranty, 175 | indemnity, or other liability obligations and/or rights consistent with 176 | this License. However, in accepting such obligations, You may act only 177 | on Your own behalf and on Your sole responsibility, not on behalf of any 178 | other Contributor, and only if You agree to indemnify, defend, and hold 179 | each Contributor harmless for any liability incurred by, or claims 180 | asserted against, such Contributor by reason of your accepting any such 181 | warranty or additional liability. 182 | 183 | END OF TERMS AND CONDITIONS 184 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | This repository is archived and will not receive any updates 2 | ############################################################ 3 | 4 | It's time to say goodbye. I'm archiving Slacker. It's been getting harder to find time to maintain this project for a while now. For years it's been the most popular Python library for Slack. Eventually Slack decided to go with `their library `_, and I lost my motivation to maintain it. Thank you all for your contributions to this project. 5 | 6 | ======= 7 | Slacker 8 | ======= 9 | 10 | |pypi|_ 11 | |build status|_ 12 | |pypi downloads|_ 13 | |license|_ 14 | |gitter chat|_ 15 | 16 | .. image:: https://raw.githubusercontent.com/os/slacker/master/static/slacker.jpg 17 | 18 | About 19 | ===== 20 | 21 | Slacker is a full-featured Python interface for the `Slack API 22 | `_. 23 | 24 | Installation 25 | ============ 26 | 27 | .. code-block:: bash 28 | 29 | $ pip install slacker 30 | 31 | Examples 32 | ======== 33 | .. code-block:: python 34 | 35 | from slacker import Slacker 36 | 37 | slack = Slacker('') 38 | 39 | # Send a message to #general channel 40 | slack.chat.post_message('#general', 'Hello fellow slackers!') 41 | 42 | # Get users list 43 | response = slack.users.list() 44 | users = response.body['members'] 45 | 46 | # Upload a file 47 | slack.files.upload('hello.txt') 48 | 49 | # If you need to proxy the requests 50 | proxy_endpoint = 'http://myproxy:3128' 51 | slack = Slacker('', 52 | http_proxy=proxy_endpoint, 53 | https_proxy=proxy_endpoint) 54 | 55 | # Advanced: Use `request.Session` for connection pooling (reuse) 56 | from requests.sessions import Session 57 | with Session() as session: 58 | slack = Slacker(token, session=session) 59 | slack.chat.post_message('#general', 'All these requests') 60 | slack.chat.post_message('#general', 'go through') 61 | slack.chat.post_message('#general', 'a single https connection') 62 | 63 | 64 | Documentation 65 | ============= 66 | 67 | https://api.slack.com/methods 68 | 69 | 70 | .. |build status| image:: https://img.shields.io/travis/os/slacker.svg 71 | .. _build status: http://travis-ci.org/os/slacker 72 | .. |pypi downloads| image:: https://img.shields.io/pypi/dm/slacker.svg 73 | .. _pypi downloads: https://pypi.org/project/slacker/ 74 | .. |pypi| image:: https://img.shields.io/pypi/v/Slacker.svg 75 | .. _pypi: https://pypi.python.org/pypi/slacker/ 76 | .. |license| image:: https://img.shields.io/github/license/os/slacker.svg 77 | .. _license: https://pypi.org/project/slacker/ 78 | .. |gitter chat| image:: https://badges.gitter.im/Join%20Chat.svg 79 | .. _gitter chat: https://gitter.im/os/slacker?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 80 | -------------------------------------------------------------------------------- /examples/list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """List items in slack.""" 3 | 4 | # https://github.com/os/slacker 5 | # https://api.slack.com/methods 6 | 7 | import os 8 | from slacker import Slacker 9 | 10 | 11 | def list_slack(): 12 | """List channels & users in slack.""" 13 | try: 14 | token = os.environ['SLACK_TOKEN'] 15 | slack = Slacker(token) 16 | 17 | # Get channel list 18 | response = slack.channels.list() 19 | channels = response.body['channels'] 20 | for channel in channels: 21 | print(channel['id'], channel['name']) 22 | # if not channel['is_archived']: 23 | # slack.channels.join(channel['name']) 24 | print() 25 | 26 | # Get users list 27 | response = slack.users.list() 28 | users = response.body['members'] 29 | for user in users: 30 | if not user['deleted']: 31 | print(user['id'], user['name'], user['is_admin'], user[ 32 | 'is_owner']) 33 | print() 34 | except KeyError as ex: 35 | print('Environment variable %s not set.' % str(ex)) 36 | 37 | 38 | if __name__ == '__main__': 39 | list_slack() 40 | -------------------------------------------------------------------------------- /examples/post.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Post slack message.""" 3 | 4 | # https://github.com/os/slacker 5 | # https://api.slack.com/methods 6 | 7 | import os 8 | from slacker import Slacker 9 | 10 | 11 | def post_slack(): 12 | """Post slack message.""" 13 | try: 14 | token = os.environ['SLACK_TOKEN'] 15 | slack = Slacker(token) 16 | 17 | obj = slack.chat.post_message('#general', 'Hello fellow slackers!') 18 | print(obj.successful, obj.__dict__['body']['channel'], obj.__dict__[ 19 | 'body']['ts']) 20 | except KeyError as ex: 21 | print('Environment variable %s not set.' % str(ex)) 22 | 23 | 24 | if __name__ == '__main__': 25 | post_slack() 26 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.3.9 2 | chardet==3.0.4 3 | filelock==3.0.10 4 | idna==2.8 5 | pluggy==0.9.0 6 | py==1.8.0 7 | requests>=2.2.1 8 | responses==0.10.6 9 | six==1.12.0 10 | toml==0.10.0 11 | tox==3.9.0 12 | urllib3==1.24.2 13 | virtualenv==16.4.3 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests >= 2.2.1 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | with open('README.rst') as f: 5 | readme = f.read() 6 | 7 | 8 | setup( 9 | name='slacker', 10 | version='0.14.0', 11 | packages=['slacker'], 12 | description='Slack API client', 13 | long_description=readme, 14 | long_description_content_type='text/x-rst', 15 | author='Oktay Sancak', 16 | author_email='oktaysancak@gmail.com', 17 | url='http://github.com/os/slacker/', 18 | install_requires=['requests >= 2.2.1'], 19 | license='http://www.apache.org/licenses/LICENSE-2.0', 20 | test_suite='tests', 21 | classifiers=[ 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: Apache Software License', 24 | 'Natural Language :: English', 25 | 'Programming Language :: Python', 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3.3', 29 | 'Programming Language :: Python :: 3.4', 30 | 'Programming Language :: Python :: 3.5', 31 | 'Programming Language :: Python :: 3.6', 32 | 'Programming Language :: Python :: 3.7', 33 | ], 34 | keywords='slack api' 35 | ) 36 | -------------------------------------------------------------------------------- /slacker/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Oktay Sancak 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | 17 | import requests 18 | 19 | import time 20 | 21 | from slacker.utilities import ( 22 | get_api_url, 23 | get_item_id_by_name, 24 | ) 25 | 26 | 27 | __version__ = '0.14.0' 28 | 29 | DEFAULT_TIMEOUT = 10 30 | DEFAULT_RETRIES = 0 31 | # seconds to wait after a 429 error if Slack's API doesn't provide one 32 | DEFAULT_WAIT = 20 33 | 34 | __all__ = ['Error', 'Response', 'BaseAPI', 'API', 'Auth', 'Users', 'Groups', 35 | 'Channels', 'Chat', 'IM', 'IncomingWebhook', 'Search', 'Files', 36 | 'Stars', 'Emoji', 'Presence', 'RTM', 'Team', 'Reactions', 'Pins', 37 | 'UserGroups', 'UserGroupsUsers', 'MPIM', 'OAuth', 'DND', 'Bots', 38 | 'FilesComments', 'Reminders', 'TeamProfile', 'UsersProfile', 39 | 'IDPGroups', 'Apps', 'AppsPermissions', 'Slacker', 'Dialog', 40 | 'Conversations', 'Migration'] 41 | 42 | 43 | class Error(Exception): 44 | pass 45 | 46 | 47 | class Response(object): 48 | def __init__(self, body): 49 | self.raw = body 50 | self.body = json.loads(body) 51 | self.successful = self.body['ok'] 52 | self.error = self.body.get('error') 53 | 54 | def __str__(self): 55 | return json.dumps(self.body) 56 | 57 | 58 | class BaseAPI(object): 59 | def __init__(self, token=None, timeout=DEFAULT_TIMEOUT, proxies=None, 60 | session=None, rate_limit_retries=DEFAULT_RETRIES): 61 | self.token = token 62 | self.timeout = timeout 63 | self.proxies = proxies 64 | self.session = session 65 | self.rate_limit_retries = rate_limit_retries 66 | 67 | def _request(self, request_method, method, **kwargs): 68 | if self.token: 69 | kwargs.setdefault('params', {})['token'] = self.token 70 | 71 | url = get_api_url(method) 72 | 73 | # while we have rate limit retries left, fetch the resource and back 74 | # off as Slack's HTTP response suggests 75 | for retry_num in range(self.rate_limit_retries): 76 | response = request_method( 77 | url, timeout=self.timeout, proxies=self.proxies, **kwargs 78 | ) 79 | 80 | if response.status_code == requests.codes.ok: 81 | break 82 | 83 | # handle HTTP 429 as documented at 84 | # https://api.slack.com/docs/rate-limits 85 | if response.status_code == requests.codes.too_many: 86 | time.sleep(int( 87 | response.headers.get('retry-after', DEFAULT_WAIT) 88 | )) 89 | continue 90 | 91 | response.raise_for_status() 92 | else: 93 | # with no retries left, make one final attempt to fetch the 94 | # resource, but do not handle too_many status differently 95 | response = request_method( 96 | url, timeout=self.timeout, proxies=self.proxies, **kwargs 97 | ) 98 | response.raise_for_status() 99 | 100 | response = Response(response.text) 101 | if not response.successful: 102 | raise Error(response.error) 103 | 104 | return response 105 | 106 | def _session_get(self, url, params=None, **kwargs): 107 | kwargs.setdefault('allow_redirects', True) 108 | return self.session.request( 109 | method='get', url=url, params=params, **kwargs 110 | ) 111 | 112 | def _session_post(self, url, data=None, **kwargs): 113 | return self.session.request( 114 | method='post', url=url, data=data, **kwargs 115 | ) 116 | 117 | def get(self, api, **kwargs): 118 | return self._request( 119 | self._session_get if self.session else requests.get, 120 | api, **kwargs 121 | ) 122 | 123 | def post(self, api, **kwargs): 124 | return self._request( 125 | self._session_post if self.session else requests.post, 126 | api, **kwargs 127 | ) 128 | 129 | 130 | class API(BaseAPI): 131 | def test(self, error=None, **kwargs): 132 | if error: 133 | kwargs['error'] = error 134 | 135 | return self.get('api.test', params=kwargs) 136 | 137 | 138 | class Auth(BaseAPI): 139 | def test(self): 140 | return self.get('auth.test') 141 | 142 | def revoke(self, test=True): 143 | return self.post('auth.revoke', data={'test': int(test)}) 144 | 145 | 146 | class Conversations(BaseAPI): 147 | def archive(self, channel): 148 | return self.post('conversations.archive', data={'channel': channel}) 149 | 150 | def close(self, channel): 151 | return self.post('conversations.close', data={'channel': channel}) 152 | 153 | def create(self, name, user_ids=None, is_private=None): 154 | if isinstance(user_ids, (list, tuple)): 155 | user_ids = ','.join(user_ids) 156 | 157 | return self.post( 158 | 'conversations.create', 159 | data={'name': name, 'user_ids': user_ids, 'is_private': is_private} 160 | ) 161 | 162 | def history(self, channel, cursor=None, inclusive=None, latest=None, 163 | oldest=None, limit=None): 164 | return self.get( 165 | 'conversations.history', 166 | params={ 167 | 'channel': channel, 168 | 'cursor': cursor, 169 | 'inclusive': inclusive, 170 | 'latest': latest, 171 | 'oldest': oldest, 172 | 'limit': limit 173 | } 174 | ) 175 | 176 | def info(self, channel, include_locale=None, include_num_members=None): 177 | return self.get( 178 | 'conversations.info', 179 | params={ 180 | 'channel': channel, 181 | 'include_locale': include_locale, 182 | 'include_num_members': include_num_members 183 | } 184 | ) 185 | 186 | def invite(self, channel, users): 187 | if isinstance(users, (list, tuple)): 188 | users = ','.join(users) 189 | 190 | return self.post( 191 | 'conversations.invite', 192 | data={'channel': channel, 'users': users} 193 | ) 194 | 195 | def join(self, channel): 196 | return self.post('conversations.join', data={'channel': channel}) 197 | 198 | def kick(self, channel, user): 199 | return self.post( 200 | 'conversations.kick', 201 | data={'channel': channel, 'user': user} 202 | ) 203 | 204 | def leave(self, channel): 205 | return self.post('conversations.leave', data={'channel': channel}) 206 | 207 | def list(self, cursor=None, exclude_archived=None, types=None, limit=None): 208 | if isinstance(types, (list, tuple)): 209 | types = ','.join(types) 210 | 211 | return self.get( 212 | 'conversations.list', 213 | params={ 214 | 'cursor': cursor, 215 | 'exclude_archived': exclude_archived, 216 | 'types': types, 217 | 'limit': limit 218 | } 219 | ) 220 | 221 | def members(self, channel, cursor=None, limit=None): 222 | return self.get( 223 | 'conversations.members', 224 | params={'channel': channel, 'cursor': cursor, 'limit': limit} 225 | ) 226 | 227 | def open(self, channel=None, users=None, return_im=None): 228 | if isinstance(users, (list, tuple)): 229 | users = ','.join(users) 230 | 231 | return self.post( 232 | 'conversations.open', 233 | data={'channel': channel, 'users': users, 'return_im': return_im} 234 | ) 235 | 236 | def rename(self, channel, name): 237 | return self.post( 238 | 'conversations.rename', 239 | data={'channel': channel, 'name': name} 240 | ) 241 | 242 | def replies(self, channel, ts, cursor=None, inclusive=None, latest=None, 243 | oldest=None, limit=None): 244 | return self.get( 245 | 'conversations.replies', 246 | params={ 247 | 'channel': channel, 248 | 'ts': ts, 249 | 'cursor': cursor, 250 | 'inclusive': inclusive, 251 | 'latest': latest, 252 | 'oldest': oldest, 253 | 'limit': limit 254 | } 255 | ) 256 | 257 | def set_purpose(self, channel, purpose): 258 | return self.post( 259 | 'conversations.setPurpose', 260 | data={'channel': channel, 'purpose': purpose} 261 | ) 262 | 263 | def set_topic(self, channel, topic): 264 | return self.post( 265 | 'conversations.setTopic', 266 | data={'channel': channel, 'topic': topic} 267 | ) 268 | 269 | def unarchive(self, channel): 270 | return self.post('conversations.unarchive', data={'channel': channel}) 271 | 272 | 273 | class Dialog(BaseAPI): 274 | def open(self, dialog, trigger_id): 275 | return self.post('dialog.open', 276 | data={ 277 | 'dialog': json.dumps(dialog), 278 | 'trigger_id': trigger_id, 279 | }) 280 | 281 | 282 | class UsersProfile(BaseAPI): 283 | def get(self, user=None, include_labels=False): 284 | return super(UsersProfile, self).get( 285 | 'users.profile.get', 286 | params={'user': user, 'include_labels': int(include_labels)} 287 | ) 288 | 289 | def set(self, user=None, profile=None, name=None, value=None): 290 | return self.post('users.profile.set', 291 | data={ 292 | 'user': user, 293 | 'profile': profile, 294 | 'name': name, 295 | 'value': value 296 | }) 297 | 298 | 299 | class UsersAdmin(BaseAPI): 300 | def invite(self, email, channels=None, first_name=None, 301 | last_name=None, resend=True): 302 | return self.post('users.admin.invite', 303 | params={ 304 | 'email': email, 305 | 'channels': channels, 306 | 'first_name': first_name, 307 | 'last_name': last_name, 308 | 'resend': resend 309 | }) 310 | 311 | 312 | class Users(BaseAPI): 313 | def __init__(self, *args, **kwargs): 314 | super(Users, self).__init__(*args, **kwargs) 315 | self._profile = UsersProfile(*args, **kwargs) 316 | self._admin = UsersAdmin(*args, **kwargs) 317 | 318 | @property 319 | def profile(self): 320 | return self._profile 321 | 322 | @property 323 | def admin(self): 324 | return self._admin 325 | 326 | def info(self, user, include_locale=False): 327 | return self.get('users.info', 328 | params={'user': user, 'include_locale': include_locale}) 329 | 330 | def list(self, presence=False): 331 | return self.get('users.list', params={'presence': int(presence)}) 332 | 333 | def identity(self): 334 | return self.get('users.identity') 335 | 336 | def set_active(self): 337 | return self.post('users.setActive') 338 | 339 | def get_presence(self, user): 340 | return self.get('users.getPresence', params={'user': user}) 341 | 342 | def set_presence(self, presence): 343 | return self.post('users.setPresence', data={'presence': presence}) 344 | 345 | def get_user_id(self, user_name): 346 | members = self.list().body['members'] 347 | return get_item_id_by_name(members, user_name) 348 | 349 | 350 | class Groups(BaseAPI): 351 | def create(self, name): 352 | return self.post('groups.create', data={'name': name}) 353 | 354 | def create_child(self, channel): 355 | return self.post('groups.createChild', data={'channel': channel}) 356 | 357 | def info(self, channel): 358 | return self.get('groups.info', params={'channel': channel}) 359 | 360 | def list(self, exclude_archived=None): 361 | return self.get('groups.list', 362 | params={'exclude_archived': exclude_archived}) 363 | 364 | def history(self, channel, latest=None, oldest=None, count=None, 365 | inclusive=None): 366 | return self.get('groups.history', 367 | params={ 368 | 'channel': channel, 369 | 'latest': latest, 370 | 'oldest': oldest, 371 | 'count': count, 372 | 'inclusive': inclusive 373 | }) 374 | 375 | def invite(self, channel, user): 376 | return self.post('groups.invite', 377 | data={'channel': channel, 'user': user}) 378 | 379 | def kick(self, channel, user): 380 | return self.post('groups.kick', 381 | data={'channel': channel, 'user': user}) 382 | 383 | def leave(self, channel): 384 | return self.post('groups.leave', data={'channel': channel}) 385 | 386 | def mark(self, channel, ts): 387 | return self.post('groups.mark', data={'channel': channel, 'ts': ts}) 388 | 389 | def rename(self, channel, name): 390 | return self.post('groups.rename', 391 | data={'channel': channel, 'name': name}) 392 | 393 | def replies(self, channel, thread_ts): 394 | return self.get('groups.replies', 395 | params={'channel': channel, 'thread_ts': thread_ts}) 396 | 397 | def archive(self, channel): 398 | return self.post('groups.archive', data={'channel': channel}) 399 | 400 | def unarchive(self, channel): 401 | return self.post('groups.unarchive', data={'channel': channel}) 402 | 403 | def open(self, channel): 404 | return self.post('groups.open', data={'channel': channel}) 405 | 406 | def close(self, channel): 407 | return self.post('groups.close', data={'channel': channel}) 408 | 409 | def set_purpose(self, channel, purpose): 410 | return self.post('groups.setPurpose', 411 | data={'channel': channel, 'purpose': purpose}) 412 | 413 | def set_topic(self, channel, topic): 414 | return self.post('groups.setTopic', 415 | data={'channel': channel, 'topic': topic}) 416 | 417 | 418 | class Channels(BaseAPI): 419 | def create(self, name): 420 | return self.post('channels.create', data={'name': name}) 421 | 422 | def info(self, channel): 423 | return self.get('channels.info', params={'channel': channel}) 424 | 425 | def list(self, exclude_archived=None, exclude_members=None): 426 | return self.get('channels.list', 427 | params={'exclude_archived': exclude_archived, 428 | 'exclude_members': exclude_members}) 429 | 430 | def history(self, channel, latest=None, oldest=None, count=None, 431 | inclusive=False, unreads=False): 432 | return self.get('channels.history', 433 | params={ 434 | 'channel': channel, 435 | 'latest': latest, 436 | 'oldest': oldest, 437 | 'count': count, 438 | 'inclusive': int(inclusive), 439 | 'unreads': int(unreads) 440 | }) 441 | 442 | def mark(self, channel, ts): 443 | return self.post('channels.mark', 444 | data={'channel': channel, 'ts': ts}) 445 | 446 | def join(self, name): 447 | return self.post('channels.join', data={'name': name}) 448 | 449 | def leave(self, channel): 450 | return self.post('channels.leave', data={'channel': channel}) 451 | 452 | def invite(self, channel, user): 453 | return self.post('channels.invite', 454 | data={'channel': channel, 'user': user}) 455 | 456 | def kick(self, channel, user): 457 | return self.post('channels.kick', 458 | data={'channel': channel, 'user': user}) 459 | 460 | def rename(self, channel, name): 461 | return self.post('channels.rename', 462 | data={'channel': channel, 'name': name}) 463 | 464 | def replies(self, channel, thread_ts): 465 | return self.get('channels.replies', 466 | params={'channel': channel, 'thread_ts': thread_ts}) 467 | 468 | def archive(self, channel): 469 | return self.post('channels.archive', data={'channel': channel}) 470 | 471 | def unarchive(self, channel): 472 | return self.post('channels.unarchive', data={'channel': channel}) 473 | 474 | def set_purpose(self, channel, purpose): 475 | return self.post('channels.setPurpose', 476 | data={'channel': channel, 'purpose': purpose}) 477 | 478 | def set_topic(self, channel, topic): 479 | return self.post('channels.setTopic', 480 | data={'channel': channel, 'topic': topic}) 481 | 482 | def get_channel_id(self, channel_name): 483 | channels = self.list().body['channels'] 484 | return get_item_id_by_name(channels, channel_name) 485 | 486 | 487 | class Chat(BaseAPI): 488 | def post_message(self, channel, text=None, username=None, as_user=None, 489 | parse=None, link_names=None, attachments=None, 490 | unfurl_links=None, unfurl_media=None, icon_url=None, 491 | icon_emoji=None, thread_ts=None, reply_broadcast=None, 492 | blocks=None, mrkdwn=True): 493 | 494 | # Ensure attachments are json encoded 495 | if attachments: 496 | if isinstance(attachments, list): 497 | attachments = json.dumps(attachments) 498 | 499 | return self.post('chat.postMessage', 500 | data={ 501 | 'channel': channel, 502 | 'text': text, 503 | 'username': username, 504 | 'as_user': as_user, 505 | 'parse': parse, 506 | 'link_names': link_names, 507 | 'attachments': attachments, 508 | 'unfurl_links': unfurl_links, 509 | 'unfurl_media': unfurl_media, 510 | 'icon_url': icon_url, 511 | 'icon_emoji': icon_emoji, 512 | 'thread_ts': thread_ts, 513 | 'reply_broadcast': reply_broadcast, 514 | 'blocks': blocks, 515 | 'mrkdwn': mrkdwn, 516 | }) 517 | 518 | def me_message(self, channel, text): 519 | return self.post('chat.meMessage', 520 | data={'channel': channel, 'text': text}) 521 | 522 | def command(self, channel, command, text): 523 | return self.post('chat.command', 524 | data={ 525 | 'channel': channel, 526 | 'command': command, 527 | 'text': text 528 | }) 529 | 530 | def update(self, channel, ts, text, attachments=None, parse=None, 531 | link_names=False, as_user=None, blocks=None): 532 | # Ensure attachments are json encoded 533 | if attachments is not None and isinstance(attachments, list): 534 | attachments = json.dumps(attachments) 535 | return self.post('chat.update', 536 | data={ 537 | 'channel': channel, 538 | 'ts': ts, 539 | 'text': text, 540 | 'attachments': attachments, 541 | 'parse': parse, 542 | 'link_names': int(link_names), 543 | 'as_user': as_user, 544 | 'blocks': blocks 545 | }) 546 | 547 | def delete(self, channel, ts, as_user=False): 548 | return self.post('chat.delete', 549 | data={ 550 | 'channel': channel, 551 | 'ts': ts, 552 | 'as_user': as_user 553 | }) 554 | 555 | def post_ephemeral(self, channel, text, user, as_user=None, 556 | attachments=None, link_names=None, parse=None, 557 | blocks=None): 558 | # Ensure attachments are json encoded 559 | if attachments is not None and isinstance(attachments, list): 560 | attachments = json.dumps(attachments) 561 | return self.post('chat.postEphemeral', 562 | data={ 563 | 'channel': channel, 564 | 'text': text, 565 | 'user': user, 566 | 'as_user': as_user, 567 | 'attachments': attachments, 568 | 'link_names': link_names, 569 | 'parse': parse, 570 | 'blocks': blocks 571 | }) 572 | 573 | def unfurl(self, channel, ts, unfurls, user_auth_message=None, 574 | user_auth_required=False, user_auth_url=None): 575 | return self.post('chat.unfurl', 576 | data={ 577 | 'channel': channel, 578 | 'ts': ts, 579 | 'unfurls': unfurls, 580 | 'user_auth_message': user_auth_message, 581 | 'user_auth_required': user_auth_required, 582 | 'user_auth_url': user_auth_url, 583 | }) 584 | 585 | def get_permalink(self, channel, message_ts): 586 | return self.get('chat.getPermalink', 587 | params={ 588 | 'channel': channel, 589 | 'message_ts': message_ts 590 | }) 591 | 592 | 593 | class IM(BaseAPI): 594 | def list(self): 595 | return self.get('im.list') 596 | 597 | def history(self, channel, latest=None, oldest=None, count=None, 598 | inclusive=None, unreads=False): 599 | return self.get('im.history', 600 | params={ 601 | 'channel': channel, 602 | 'latest': latest, 603 | 'oldest': oldest, 604 | 'count': count, 605 | 'inclusive': inclusive, 606 | 'unreads': int(unreads) 607 | }) 608 | 609 | def replies(self, channel, thread_ts): 610 | return self.get('im.replies', 611 | params={'channel': channel, 'thread_ts': thread_ts}) 612 | 613 | def mark(self, channel, ts): 614 | return self.post('im.mark', data={'channel': channel, 'ts': ts}) 615 | 616 | def open(self, user): 617 | return self.post('im.open', data={'user': user}) 618 | 619 | def close(self, channel): 620 | return self.post('im.close', data={'channel': channel}) 621 | 622 | 623 | class MPIM(BaseAPI): 624 | def open(self, users): 625 | if isinstance(users, (tuple, list)): 626 | users = ','.join(users) 627 | 628 | return self.post('mpim.open', data={'users': users}) 629 | 630 | def close(self, channel): 631 | return self.post('mpim.close', data={'channel': channel}) 632 | 633 | def mark(self, channel, ts): 634 | return self.post('mpim.mark', data={'channel': channel, 'ts': ts}) 635 | 636 | def list(self): 637 | return self.get('mpim.list') 638 | 639 | def history(self, channel, latest=None, oldest=None, inclusive=False, 640 | count=None, unreads=False): 641 | return self.get('mpim.history', 642 | params={ 643 | 'channel': channel, 644 | 'latest': latest, 645 | 'oldest': oldest, 646 | 'inclusive': int(inclusive), 647 | 'count': count, 648 | 'unreads': int(unreads) 649 | }) 650 | 651 | def replies(self, channel, thread_ts): 652 | return self.get('mpim.replies', 653 | params={'channel': channel, 'thread_ts': thread_ts}) 654 | 655 | 656 | class Search(BaseAPI): 657 | def all(self, query, sort=None, sort_dir=None, highlight=None, count=None, 658 | page=None): 659 | return self.get('search.all', 660 | params={ 661 | 'query': query, 662 | 'sort': sort, 663 | 'sort_dir': sort_dir, 664 | 'highlight': highlight, 665 | 'count': count, 666 | 'page': page 667 | }) 668 | 669 | def files(self, query, sort=None, sort_dir=None, highlight=None, 670 | count=None, page=None): 671 | return self.get('search.files', 672 | params={ 673 | 'query': query, 674 | 'sort': sort, 675 | 'sort_dir': sort_dir, 676 | 'highlight': highlight, 677 | 'count': count, 678 | 'page': page 679 | }) 680 | 681 | def messages(self, query, sort=None, sort_dir=None, highlight=None, 682 | count=None, page=None): 683 | return self.get('search.messages', 684 | params={ 685 | 'query': query, 686 | 'sort': sort, 687 | 'sort_dir': sort_dir, 688 | 'highlight': highlight, 689 | 'count': count, 690 | 'page': page 691 | }) 692 | 693 | 694 | class FilesComments(BaseAPI): 695 | def add(self, file_, comment): 696 | return self.post('files.comments.add', 697 | data={'file': file_, 'comment': comment}) 698 | 699 | def delete(self, file_, id_): 700 | return self.post('files.comments.delete', 701 | data={'file': file_, 'id': id_}) 702 | 703 | def edit(self, file_, id_, comment): 704 | return self.post('files.comments.edit', 705 | data={'file': file_, 'id': id_, 'comment': comment}) 706 | 707 | 708 | class Files(BaseAPI): 709 | def __init__(self, *args, **kwargs): 710 | super(Files, self).__init__(*args, **kwargs) 711 | self._comments = FilesComments(*args, **kwargs) 712 | 713 | @property 714 | def comments(self): 715 | return self._comments 716 | 717 | def list(self, user=None, ts_from=None, ts_to=None, types=None, 718 | count=None, page=None, channel=None): 719 | return self.get('files.list', 720 | params={ 721 | 'user': user, 722 | 'ts_from': ts_from, 723 | 'ts_to': ts_to, 724 | 'types': types, 725 | 'count': count, 726 | 'page': page, 727 | 'channel': channel 728 | }) 729 | 730 | def info(self, file_, count=None, page=None): 731 | return self.get('files.info', 732 | params={'file': file_, 'count': count, 'page': page}) 733 | 734 | def upload(self, file_=None, content=None, filetype=None, filename=None, 735 | title=None, initial_comment=None, channels=None, thread_ts=None): 736 | if isinstance(channels, (tuple, list)): 737 | channels = ','.join(channels) 738 | 739 | data = { 740 | 'content': content, 741 | 'filetype': filetype, 742 | 'filename': filename, 743 | 'title': title, 744 | 'initial_comment': initial_comment, 745 | 'channels': channels, 746 | 'thread_ts': thread_ts 747 | } 748 | 749 | if file_: 750 | if isinstance(file_, str): 751 | with open(file_, 'rb') as f: 752 | return self.post( 753 | 'files.upload', data=data, files={'file': f} 754 | ) 755 | 756 | return self.post( 757 | 'files.upload', data=data, files={'file': file_} 758 | ) 759 | 760 | return self.post('files.upload', data=data) 761 | 762 | def delete(self, file_): 763 | return self.post('files.delete', data={'file': file_}) 764 | 765 | def revoke_public_url(self, file_): 766 | return self.post('files.revokePublicURL', data={'file': file_}) 767 | 768 | def shared_public_url(self, file_): 769 | return self.post('files.sharedPublicURL', data={'file': file_}) 770 | 771 | 772 | class Stars(BaseAPI): 773 | def add(self, file_=None, file_comment=None, channel=None, timestamp=None): 774 | assert file_ or file_comment or channel 775 | 776 | return self.post('stars.add', 777 | data={ 778 | 'file': file_, 779 | 'file_comment': file_comment, 780 | 'channel': channel, 781 | 'timestamp': timestamp 782 | }) 783 | 784 | def list(self, user=None, count=None, page=None): 785 | return self.get('stars.list', 786 | params={'user': user, 'count': count, 'page': page}) 787 | 788 | def remove(self, file_=None, file_comment=None, channel=None, 789 | timestamp=None): 790 | assert file_ or file_comment or channel 791 | 792 | return self.post('stars.remove', 793 | data={ 794 | 'file': file_, 795 | 'file_comment': file_comment, 796 | 'channel': channel, 797 | 'timestamp': timestamp 798 | }) 799 | 800 | 801 | class Emoji(BaseAPI): 802 | def list(self): 803 | return self.get('emoji.list') 804 | 805 | 806 | class Presence(BaseAPI): 807 | AWAY = 'away' 808 | ACTIVE = 'active' 809 | TYPES = (AWAY, ACTIVE) 810 | 811 | def set(self, presence): 812 | assert presence in Presence.TYPES, 'Invalid presence type' 813 | return self.post('presence.set', data={'presence': presence}) 814 | 815 | 816 | class RTM(BaseAPI): 817 | def start(self, simple_latest=False, no_unreads=False, mpim_aware=False): 818 | return self.get('rtm.start', 819 | params={ 820 | 'simple_latest': int(simple_latest), 821 | 'no_unreads': int(no_unreads), 822 | 'mpim_aware': int(mpim_aware), 823 | }) 824 | 825 | def connect(self): 826 | return self.get('rtm.connect') 827 | 828 | 829 | class TeamProfile(BaseAPI): 830 | def get(self, visibility=None): 831 | return super(TeamProfile, self).get( 832 | 'team.profile.get', 833 | params={'visibility': visibility} 834 | ) 835 | 836 | 837 | class Team(BaseAPI): 838 | def __init__(self, *args, **kwargs): 839 | super(Team, self).__init__(*args, **kwargs) 840 | self._profile = TeamProfile(*args, **kwargs) 841 | 842 | @property 843 | def profile(self): 844 | return self._profile 845 | 846 | def info(self): 847 | return self.get('team.info') 848 | 849 | def access_logs(self, count=None, page=None, before=None): 850 | return self.get('team.accessLogs', 851 | params={ 852 | 'count': count, 853 | 'page': page, 854 | 'before': before 855 | }) 856 | 857 | def integration_logs(self, service_id=None, app_id=None, user=None, 858 | change_type=None, count=None, page=None): 859 | return self.get('team.integrationLogs', 860 | params={ 861 | 'service_id': service_id, 862 | 'app_id': app_id, 863 | 'user': user, 864 | 'change_type': change_type, 865 | 'count': count, 866 | 'page': page, 867 | }) 868 | 869 | def billable_info(self, user=None): 870 | return self.get('team.billableInfo', params={'user': user}) 871 | 872 | 873 | class Reactions(BaseAPI): 874 | def add(self, name, file_=None, file_comment=None, channel=None, 875 | timestamp=None): 876 | # One of file, file_comment, or the combination of channel and timestamp 877 | # must be specified 878 | assert (file_ or file_comment) or (channel and timestamp) 879 | 880 | return self.post('reactions.add', 881 | data={ 882 | 'name': name, 883 | 'file': file_, 884 | 'file_comment': file_comment, 885 | 'channel': channel, 886 | 'timestamp': timestamp, 887 | }) 888 | 889 | def get(self, file_=None, file_comment=None, channel=None, timestamp=None, 890 | full=None): 891 | return super(Reactions, self).get('reactions.get', 892 | params={ 893 | 'file': file_, 894 | 'file_comment': file_comment, 895 | 'channel': channel, 896 | 'timestamp': timestamp, 897 | 'full': full, 898 | }) 899 | 900 | def list(self, user=None, full=None, count=None, page=None): 901 | return super(Reactions, self).get('reactions.list', 902 | params={ 903 | 'user': user, 904 | 'full': full, 905 | 'count': count, 906 | 'page': page, 907 | }) 908 | 909 | def remove(self, name, file_=None, file_comment=None, channel=None, 910 | timestamp=None): 911 | # One of file, file_comment, or the combination of channel and timestamp 912 | # must be specified 913 | assert (file_ or file_comment) or (channel and timestamp) 914 | 915 | return self.post('reactions.remove', 916 | data={ 917 | 'name': name, 918 | 'file': file_, 919 | 'file_comment': file_comment, 920 | 'channel': channel, 921 | 'timestamp': timestamp, 922 | }) 923 | 924 | 925 | class Pins(BaseAPI): 926 | def add(self, channel, file_=None, file_comment=None, timestamp=None): 927 | # One of file, file_comment, or timestamp must also be specified 928 | assert file_ or file_comment or timestamp 929 | 930 | return self.post('pins.add', 931 | data={ 932 | 'channel': channel, 933 | 'file': file_, 934 | 'file_comment': file_comment, 935 | 'timestamp': timestamp, 936 | }) 937 | 938 | def remove(self, channel, file_=None, file_comment=None, timestamp=None): 939 | # One of file, file_comment, or timestamp must also be specified 940 | assert file_ or file_comment or timestamp 941 | 942 | return self.post('pins.remove', 943 | data={ 944 | 'channel': channel, 945 | 'file': file_, 946 | 'file_comment': file_comment, 947 | 'timestamp': timestamp, 948 | }) 949 | 950 | def list(self, channel): 951 | return self.get('pins.list', params={'channel': channel}) 952 | 953 | 954 | class UserGroupsUsers(BaseAPI): 955 | def list(self, usergroup, include_disabled=None): 956 | if isinstance(include_disabled, bool): 957 | include_disabled = int(include_disabled) 958 | 959 | return self.get('usergroups.users.list', params={ 960 | 'usergroup': usergroup, 961 | 'include_disabled': include_disabled, 962 | }) 963 | 964 | def update(self, usergroup, users, include_count=None): 965 | if isinstance(users, (tuple, list)): 966 | users = ','.join(users) 967 | 968 | if isinstance(include_count, bool): 969 | include_count = int(include_count) 970 | 971 | return self.post('usergroups.users.update', data={ 972 | 'usergroup': usergroup, 973 | 'users': users, 974 | 'include_count': include_count, 975 | }) 976 | 977 | 978 | class UserGroups(BaseAPI): 979 | def __init__(self, *args, **kwargs): 980 | super(UserGroups, self).__init__(*args, **kwargs) 981 | self._users = UserGroupsUsers(*args, **kwargs) 982 | 983 | @property 984 | def users(self): 985 | return self._users 986 | 987 | def list(self, include_disabled=None, include_count=None, 988 | include_users=None): 989 | if isinstance(include_disabled, bool): 990 | include_disabled = int(include_disabled) 991 | 992 | if isinstance(include_count, bool): 993 | include_count = int(include_count) 994 | 995 | if isinstance(include_users, bool): 996 | include_users = int(include_users) 997 | 998 | return self.get('usergroups.list', params={ 999 | 'include_disabled': include_disabled, 1000 | 'include_count': include_count, 1001 | 'include_users': include_users, 1002 | }) 1003 | 1004 | def create(self, name, handle=None, description=None, channels=None, 1005 | include_count=None): 1006 | if isinstance(channels, (tuple, list)): 1007 | channels = ','.join(channels) 1008 | 1009 | if isinstance(include_count, bool): 1010 | include_count = int(include_count) 1011 | 1012 | return self.post('usergroups.create', data={ 1013 | 'name': name, 1014 | 'handle': handle, 1015 | 'description': description, 1016 | 'channels': channels, 1017 | 'include_count': include_count, 1018 | }) 1019 | 1020 | def update(self, usergroup, name=None, handle=None, description=None, 1021 | channels=None, include_count=None): 1022 | if isinstance(channels, (tuple, list)): 1023 | channels = ','.join(channels) 1024 | 1025 | if isinstance(include_count, bool): 1026 | include_count = int(include_count) 1027 | 1028 | return self.post('usergroups.update', data={ 1029 | 'usergroup': usergroup, 1030 | 'name': name, 1031 | 'handle': handle, 1032 | 'description': description, 1033 | 'channels': channels, 1034 | 'include_count': include_count, 1035 | }) 1036 | 1037 | def disable(self, usergroup, include_count=None): 1038 | if isinstance(include_count, bool): 1039 | include_count = int(include_count) 1040 | 1041 | return self.post('usergroups.disable', data={ 1042 | 'usergroup': usergroup, 1043 | 'include_count': include_count, 1044 | }) 1045 | 1046 | def enable(self, usergroup, include_count=None): 1047 | if isinstance(include_count, bool): 1048 | include_count = int(include_count) 1049 | 1050 | return self.post('usergroups.enable', data={ 1051 | 'usergroup': usergroup, 1052 | 'include_count': include_count, 1053 | }) 1054 | 1055 | 1056 | class DND(BaseAPI): 1057 | def team_info(self, users=None): 1058 | if isinstance(users, (tuple, list)): 1059 | users = ','.join(users) 1060 | 1061 | return self.get('dnd.teamInfo', params={'users': users}) 1062 | 1063 | def set_snooze(self, num_minutes): 1064 | return self.post('dnd.setSnooze', data={'num_minutes': num_minutes}) 1065 | 1066 | def info(self, user=None): 1067 | return self.get('dnd.info', params={'user': user}) 1068 | 1069 | def end_dnd(self): 1070 | return self.post('dnd.endDnd') 1071 | 1072 | def end_snooze(self): 1073 | return self.post('dnd.endSnooze') 1074 | 1075 | 1076 | class Migration(BaseAPI): 1077 | def exchange(self, users, to_old=False): 1078 | if isinstance(users, (list, tuple)): 1079 | users = ','.join(users) 1080 | 1081 | return self.get( 1082 | 'migration.exchange', params={'users': users, 'to_old': to_old} 1083 | ) 1084 | 1085 | 1086 | class Reminders(BaseAPI): 1087 | def add(self, text, time, user=None): 1088 | return self.post('reminders.add', data={ 1089 | 'text': text, 1090 | 'time': time, 1091 | 'user': user, 1092 | }) 1093 | 1094 | def complete(self, reminder): 1095 | return self.post('reminders.complete', data={'reminder': reminder}) 1096 | 1097 | def delete(self, reminder): 1098 | return self.post('reminders.delete', data={'reminder': reminder}) 1099 | 1100 | def info(self, reminder): 1101 | return self.get('reminders.info', params={'reminder': reminder}) 1102 | 1103 | def list(self): 1104 | return self.get('reminders.list') 1105 | 1106 | 1107 | class Bots(BaseAPI): 1108 | def info(self, bot=None): 1109 | return self.get('bots.info', params={'bot': bot}) 1110 | 1111 | 1112 | class IDPGroups(BaseAPI): 1113 | def list(self, include_users=False): 1114 | return self.get('idpgroups.list', 1115 | params={'include_users': int(include_users)}) 1116 | 1117 | 1118 | class OAuth(BaseAPI): 1119 | def access(self, client_id, client_secret, code, redirect_uri=None): 1120 | return self.post('oauth.access', 1121 | data={ 1122 | 'client_id': client_id, 1123 | 'client_secret': client_secret, 1124 | 'code': code, 1125 | 'redirect_uri': redirect_uri 1126 | }) 1127 | 1128 | def token(self, client_id, client_secret, code, redirect_uri=None, 1129 | single_channel=None): 1130 | return self.post('oauth.token', 1131 | data={ 1132 | 'client_id': client_id, 1133 | 'client_secret': client_secret, 1134 | 'code': code, 1135 | 'redirect_uri': redirect_uri, 1136 | 'single_channel': single_channel, 1137 | }) 1138 | 1139 | 1140 | class AppsPermissions(BaseAPI): 1141 | def info(self): 1142 | return self.get('apps.permissions.info') 1143 | 1144 | def request(self, scopes, trigger_id): 1145 | return self.post('apps.permissions.request', 1146 | data={ 1147 | scopes: ','.join(scopes), 1148 | trigger_id: trigger_id, 1149 | }) 1150 | 1151 | 1152 | class Apps(BaseAPI): 1153 | def __init__(self, *args, **kwargs): 1154 | super(Apps, self).__init__(*args, **kwargs) 1155 | self._permissions = AppsPermissions(*args, **kwargs) 1156 | 1157 | @property 1158 | def permissions(self): 1159 | return self._permissions 1160 | 1161 | def uninstall(self, client_id, client_secret): 1162 | return self.get( 1163 | 'apps.uninstall', 1164 | params={'client_id': client_id, 'client_secret': client_secret} 1165 | ) 1166 | 1167 | 1168 | class IncomingWebhook(object): 1169 | def __init__(self, url=None, timeout=DEFAULT_TIMEOUT, proxies=None): 1170 | self.url = url 1171 | self.timeout = timeout 1172 | self.proxies = proxies 1173 | 1174 | def post(self, data): 1175 | """ 1176 | Posts message with payload formatted in accordance with 1177 | this documentation https://api.slack.com/incoming-webhooks 1178 | """ 1179 | if not self.url: 1180 | raise Error('URL for incoming webhook is undefined') 1181 | 1182 | return requests.post(self.url, data=json.dumps(data), 1183 | timeout=self.timeout, proxies=self.proxies) 1184 | 1185 | 1186 | class Slacker(object): 1187 | oauth = OAuth(timeout=DEFAULT_TIMEOUT) 1188 | 1189 | def __init__(self, token, incoming_webhook_url=None, 1190 | timeout=DEFAULT_TIMEOUT, http_proxy=None, https_proxy=None, 1191 | session=None, rate_limit_retries=DEFAULT_RETRIES): 1192 | 1193 | proxies = self.__create_proxies(http_proxy, https_proxy) 1194 | api_args = { 1195 | 'token': token, 1196 | 'timeout': timeout, 1197 | 'proxies': proxies, 1198 | 'session': session, 1199 | 'rate_limit_retries': rate_limit_retries, 1200 | } 1201 | self.im = IM(**api_args) 1202 | self.api = API(**api_args) 1203 | self.dnd = DND(**api_args) 1204 | self.rtm = RTM(**api_args) 1205 | self.apps = Apps(**api_args) 1206 | self.auth = Auth(**api_args) 1207 | self.bots = Bots(**api_args) 1208 | self.chat = Chat(**api_args) 1209 | self.dialog = Dialog(**api_args) 1210 | self.team = Team(**api_args) 1211 | self.pins = Pins(**api_args) 1212 | self.mpim = MPIM(**api_args) 1213 | self.users = Users(**api_args) 1214 | self.files = Files(**api_args) 1215 | self.stars = Stars(**api_args) 1216 | self.emoji = Emoji(**api_args) 1217 | self.search = Search(**api_args) 1218 | self.groups = Groups(**api_args) 1219 | self.channels = Channels(**api_args) 1220 | self.presence = Presence(**api_args) 1221 | self.reminders = Reminders(**api_args) 1222 | self.migration = Migration(**api_args) 1223 | self.reactions = Reactions(**api_args) 1224 | self.idpgroups = IDPGroups(**api_args) 1225 | self.usergroups = UserGroups(**api_args) 1226 | self.conversations = Conversations(**api_args) 1227 | self.incomingwebhook = IncomingWebhook(url=incoming_webhook_url, 1228 | timeout=timeout, proxies=proxies) 1229 | 1230 | def __create_proxies(self, http_proxy=None, https_proxy=None): 1231 | proxies = dict() 1232 | if http_proxy: 1233 | proxies['http'] = http_proxy 1234 | if https_proxy: 1235 | proxies['https'] = https_proxy 1236 | return proxies 1237 | -------------------------------------------------------------------------------- /slacker/utilities.py: -------------------------------------------------------------------------------- 1 | def get_api_url(method): 2 | """ 3 | Returns API URL for the given method. 4 | 5 | :param method: Method name 6 | :type method: str 7 | 8 | :returns: API URL for the given method 9 | :rtype: str 10 | """ 11 | return 'https://slack.com/api/{}'.format(method) 12 | 13 | 14 | def get_item_id_by_name(list_dict, key_name): 15 | for d in list_dict: 16 | if d['name'] == key_name: 17 | return d['id'] 18 | -------------------------------------------------------------------------------- /static/slacker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os/slacker/3c7f43f75ee838be1121342c1aea1360388015e8/static/slacker.jpg -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os/slacker/3c7f43f75ee838be1121342c1aea1360388015e8/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_channels.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | import responses 6 | 7 | from slacker import Channels 8 | from slacker.utilities import get_api_url 9 | 10 | 11 | class TestChannels(unittest.TestCase): 12 | @responses.activate 13 | def test_valid_ids_return_channel_id(self): 14 | response = { 15 | 'ok': 'true', 16 | 'channels': [ 17 | {'name': 'general', 'id': 'C111'}, 18 | {'name': 'random', 'id': 'C222'} 19 | ] 20 | } 21 | responses.add( 22 | responses.GET, 23 | get_api_url('channels.list'), 24 | json=response, 25 | status=200 26 | ) 27 | channels = Channels(token='aaa') 28 | self.assertEqual(channels.get_channel_id('general'), 'C111') 29 | 30 | @responses.activate 31 | def test_invalid_channel_ids_return_none(self): 32 | response = { 33 | 'ok': 'true', 34 | 'channels': [ 35 | {'name': 'general', 'id': 'C111'}, 36 | {'name': 'random', 'id': 'C222'} 37 | ] 38 | } 39 | responses.add( 40 | responses.GET, 41 | get_api_url('channels.list'), 42 | json=response, 43 | status=200 44 | ) 45 | channels = Channels(token='aaa') 46 | self.assertEqual(channels.get_channel_id('fake_group'), None) 47 | -------------------------------------------------------------------------------- /tests/test_utilities.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from slacker.utilities import get_item_id_by_name 4 | 5 | 6 | class TestGetItemIDByName(unittest.TestCase): 7 | def test_get_item_id_by_name(self): 8 | list_dict = [{'name': 'channel_name', 'id': '123'}, {}] 9 | self.assertEqual( 10 | '123', get_item_id_by_name(list_dict, 'channel_name') 11 | ) 12 | -------------------------------------------------------------------------------- /tests/utilities.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os/slacker/3c7f43f75ee838be1121342c1aea1360388015e8/tests/utilities.py -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py27, py36, py37, py38, pypy36 8 | 9 | [testenv] 10 | deps = -r{toxinidir}/requirements-dev.txt 11 | pytest 12 | commands = py.test tests/ 13 | --------------------------------------------------------------------------------