├── .gitignore ├── LICENSE ├── README.md └── src ├── .DS_Store ├── icon.png ├── images ├── bus.png ├── circle_green.png ├── circle_grey.png ├── cross_mark.png ├── face_with_thermometer.png ├── house_with_garden.png ├── palm_tree.png └── spiral_calendar.png ├── info.plist ├── public ├── assets │ ├── img │ │ ├── alfred.png │ │ └── slack.png │ ├── script.js │ └── style.css └── index.html └── scripts ├── composer.json ├── composer.lock ├── composer.phar ├── config ├── emojis.json └── routes.php ├── controllers ├── ChannelController.php ├── ConfigController.php ├── Controller.php └── SlackController.php ├── helpers ├── core │ └── CustomCommander.php ├── http │ └── MultiCurlInteractor.php └── service │ ├── MultiTeamSlackService.php │ ├── SingleTeamSlackService.php │ └── SlackServiceInterface.php ├── index.php ├── libs ├── Bootstrap.php ├── Emoji.php ├── Query.php ├── Route.php ├── Router.php ├── SlackRouter.php ├── Utils.php └── Workflows.php └── models ├── AuthModel.php ├── ChannelModel.php ├── ChatInterface.php ├── FileModel.php ├── GroupModel.php ├── ImModel.php ├── MessageModel.php ├── Model.php ├── ModelFactory.php ├── StatusModel.php └── UserModel.php /.gitignore: -------------------------------------------------------------------------------- 1 | src/scripts/vendor/ 2 | src/.idea 3 | .idea 4 | Slack.alfredworkflow 5 | .DS_Store 6 | src/cache/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | alfred-slack 2 | ============ 3 | 4 | [![Gitter](https://badges.gitter.im/yannickglt/alfred-slack.svg)](https://gitter.im/yannickglt/alfred-slack?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 5 | 6 | Open conversation with a contact in Slack 7 | 8 | # To start 9 | 1. Download and install [Slack.alfredworkflow](https://github.com/packal/repository/raw/master/com.yannickglt.alfred2.slack/slack.alfredworkflow) 10 | 2. Create a custom app for your team following [these steps](#create-an-app-for-your-team). 11 | 3. Once you have your client ID and Secret, visit the address [https://yannickglt.github.io/alfred-slack/](https://yannickglt.github.io/alfred-slack/) to generate a unique code for authentication in the Workflow. 12 | 13 | 1. Enter your team name in the first field and your client ID in the second one, then click on the button "Generate code". 14 | 15 | ![image](https://user-images.githubusercontent.com/1006426/46915167-2494ff80-cfa8-11e8-81cd-25ff613cfdf4.png) 16 | 17 | 2. Authorize the app to access your Slack team with the rights below. 18 | 19 | ![image](https://user-images.githubusercontent.com/1006426/46915174-38d8fc80-cfa8-11e8-8aae-9b3da44db2c2.png) 20 | 21 | 3. Copy your generated unique code in your clipboard. 22 | 23 | ![image](https://user-images.githubusercontent.com/1006426/46915183-50b08080-cfa8-11e8-9a70-12fe531185e0.png) 24 | 25 | 4. Launch the slack workflow with the parameter `--add-client` followed by the concatenation of the generated unique code and client Secret separated by a colon (e.g.: `UNIQUE_CODE:CLIENT_SECRET`). 26 | You can add several clients if you want to collaborate with several teams. You just need to repeat the two last steps. 27 | 28 | Example: 29 | ``` 30 | slack --add-client 1234567890.123456789012|1234567890.123456789012.abcdef1234:1234567890abcdef1234567890abcdef 31 | ``` 32 | 5. Launch the cache refresh by taping the command `--refresh`. 33 | 34 | Example: 35 | ``` 36 | slack --refresh 37 | ``` 38 | **The cache refresh may take up to several minutes depending on your organization size.** 39 | 40 | 6. Enjoy! 41 | 42 | Note: install the [Packal Updater](http://www.packal.org/workflow/packal-updater) workflow if you want automatic updates. 43 | 44 | # How to use 45 | - List channels or groups to open in the Slack app: 46 | 47 | ``` 48 | slack 49 | ``` 50 | ![image](https://cloud.githubusercontent.com/assets/1006426/10527597/a4c81c44-7391-11e5-9009-625d1e6957f1.png) 51 | 52 | 53 | - List users to open in the Slack app: 54 | 55 | ``` 56 | slack 57 | ``` 58 | ![image](https://cloud.githubusercontent.com/assets/1006426/10527601/aa77ab3c-7391-11e5-9e04-1b937ef35206.png) 59 | 60 | - Open a channel, group or user in the Slack app: 61 | 62 | ``` 63 | slack 64 | ``` 65 | ![image](https://user-images.githubusercontent.com/1006426/29512380-5298e878-8662-11e7-9968-1ae765d4d75c.gif) 66 | 67 | - List messages from a specific channel, group or user: 68 | 69 | ``` 70 | slack 71 | ``` 72 | ![image](https://cloud.githubusercontent.com/assets/1006426/10527030/918dd7f2-738e-11e5-9ea1-4bf74a0dd9cb.png) 73 | 74 | - Send a message to a channel, group or user: 75 | 76 | ``` 77 | slack 78 | ``` 79 | ![image](https://cloud.githubusercontent.com/assets/1006426/10527561/6966d26c-7391-11e5-8907-ee2999e3ef36.png) 80 | 81 | - Mark all channels as read 82 | 83 | ``` 84 | slack --mark 85 | ``` 86 | 87 | - List the files within the team 88 | 89 | ``` 90 | slack --files 91 | ``` 92 | 93 | - List the items starred 94 | 95 | ``` 96 | slack --stars 97 | ``` 98 | 99 | - Search both messages and files 100 | 101 | ``` 102 | slack --search 103 | ``` 104 | 105 | - Set the user presence (either active or away) 106 | 107 | ``` 108 | slack --presence 109 | ``` 110 | 111 | - Set the custom status 112 | 113 | ``` 114 | slack --status 115 | ``` 116 | 117 | ### Create an app for your team 118 | 1. Go to the URL [https://api.slack.com/apps/new](https://api.slack.com/apps/new) and click on the button `Create a Slack app`. 119 | 120 | ![image](https://cloud.githubusercontent.com/assets/1006426/25681953/9067e094-3056-11e7-9f8d-4ea2b0627eff.png) 121 | 122 | 2. Give it an app name e.g.: "Alfred Workflow", select your team in the list and click on the button `Create App`. 123 | 124 | ![image](https://cloud.githubusercontent.com/assets/1006426/25682019/d5db3450-3056-11e7-8f24-470463ce6dd5.png) 125 | 126 | 3. Note the client ID and Secret! 127 | 128 | ![image](https://cloud.githubusercontent.com/assets/1006426/25771653/e8aba62a-3257-11e7-88e0-050723b7058e.png) 129 | 130 | > :warning: Never share the client secret on the web or on public repository 131 | 132 | 4. Go to the section OAuth & Permissions (under the Features category), add the two redirect URLs `http://yannickglt.github.io/alfred-slack/` and `https://yannickglt.github.io/alfred-slack/` and click on `Save URLs`. 133 | 134 | ![image](https://user-images.githubusercontent.com/1006426/29512021-5b623578-8661-11e7-96b4-6650e735b4f4.png) 135 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/.DS_Store -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/icon.png -------------------------------------------------------------------------------- /src/images/bus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/images/bus.png -------------------------------------------------------------------------------- /src/images/circle_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/images/circle_green.png -------------------------------------------------------------------------------- /src/images/circle_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/images/circle_grey.png -------------------------------------------------------------------------------- /src/images/cross_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/images/cross_mark.png -------------------------------------------------------------------------------- /src/images/face_with_thermometer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/images/face_with_thermometer.png -------------------------------------------------------------------------------- /src/images/house_with_garden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/images/house_with_garden.png -------------------------------------------------------------------------------- /src/images/palm_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/images/palm_tree.png -------------------------------------------------------------------------------- /src/images/spiral_calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/images/spiral_calendar.png -------------------------------------------------------------------------------- /src/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | com.yannickglt.alfred2.slack 7 | category 8 | Productivity 9 | connections 10 | 11 | BE4D4090-FF1C-4FF4-849C-958AFD57A03F 12 | 13 | 14 | destinationuid 15 | 43648B07-3CF9-4E47-B15C-042D2EABCDDB 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | slack 25 | 26 | 27 | destinationuid 28 | BE4D4090-FF1C-4FF4-849C-958AFD57A03F 29 | modifiers 30 | 0 31 | modifiersubtext 32 | 33 | vitoclose 34 | 35 | 36 | 37 | 38 | createdby 39 | Yannick Galatol 40 | description 41 | Interact with slack through Alfred 42 | disabled 43 | 44 | name 45 | Slack 46 | objects 47 | 48 | 49 | config 50 | 51 | lastpathcomponent 52 | 53 | onlyshowifquerypopulated 54 | 55 | removeextension 56 | 57 | text 58 | 59 | title 60 | {query} 61 | 62 | type 63 | alfred.workflow.output.notification 64 | uid 65 | 43648B07-3CF9-4E47-B15C-042D2EABCDDB 66 | version 67 | 1 68 | 69 | 70 | config 71 | 72 | alfredfiltersresults 73 | 74 | alfredfiltersresultsmatchmode 75 | 0 76 | argumenttreatemptyqueryasnil 77 | 78 | argumenttrimmode 79 | 0 80 | argumenttype 81 | 0 82 | escaping 83 | 68 84 | keyword 85 | slack 86 | queuedelaycustom 87 | 3 88 | queuedelayimmediatelyinitially 89 | 90 | queuedelaymode 91 | 0 92 | queuemode 93 | 2 94 | runningsubtext 95 | Loading... 96 | script 97 | $query = "{query}"; 98 | $input = true; 99 | require_once 'scripts/index.php'; 100 | 101 | scriptargtype 102 | 0 103 | scriptfile 104 | 105 | subtext 106 | Slack workflow 107 | title 108 | Slack Workflow 109 | type 110 | 1 111 | withspace 112 | 113 | 114 | type 115 | alfred.workflow.input.scriptfilter 116 | uid 117 | slack 118 | version 119 | 3 120 | 121 | 122 | config 123 | 124 | concurrently 125 | 126 | escaping 127 | 68 128 | script 129 | $input = false; 130 | $query = "{query}"; 131 | require_once 'scripts/index.php'; 132 | scriptargtype 133 | 0 134 | scriptfile 135 | 136 | type 137 | 1 138 | 139 | type 140 | alfred.workflow.action.script 141 | uid 142 | BE4D4090-FF1C-4FF4-849C-958AFD57A03F 143 | version 144 | 2 145 | 146 | 147 | readme 148 | 149 | uidata 150 | 151 | 43648B07-3CF9-4E47-B15C-042D2EABCDDB 152 | 153 | xpos 154 | 700 155 | ypos 156 | 10 157 | 158 | BE4D4090-FF1C-4FF4-849C-958AFD57A03F 159 | 160 | xpos 161 | 500 162 | ypos 163 | 10 164 | 165 | slack 166 | 167 | xpos 168 | 300 169 | ypos 170 | 10 171 | 172 | 173 | variablesdontexport 174 | 175 | version 176 | 4.2.2 177 | webaddress 178 | https://github.com/yannickglt/alfred-slack 179 | 180 | 181 | -------------------------------------------------------------------------------- /src/public/assets/img/alfred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/public/assets/img/alfred.png -------------------------------------------------------------------------------- /src/public/assets/img/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/public/assets/img/slack.png -------------------------------------------------------------------------------- /src/public/assets/script.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | 3 | var REDIRECT_URI = 'https://yannickglt.github.io/alfred-slack/'; 4 | var SCOPE = [ 5 | 'channels:history', 6 | 'channels:write', 7 | 'channels:read', 8 | 'chat:write:bot', 9 | 'chat:write:user', 10 | 'groups:history', 11 | 'groups:read', 12 | 'groups:write', 13 | 'files:read', 14 | 'files:write:user', 15 | 'im:history', 16 | 'im:read', 17 | 'im:write', 18 | 'search:read', 19 | 'stars:read', 20 | 'team:read', 21 | 'users.profile:write', 22 | 'users:read', 23 | 'users:write' 24 | ]; 25 | var OAUTH_URL = 'https://slack.com/oauth/authorize?client_id=__CLIENT_ID__&scope=__SCOPE__&team=__TEAM__&redirect_uri=__REDIRECT_URI__'; 26 | 27 | window.addEventListener('load', function () { 28 | 29 | var step = (getParameterByName('code') !== null) ? 2 : 1; 30 | if (step === 1) { 31 | document.getElementById('step-1').style.display = 'block'; 32 | document.getElementById('step-2').style.display = 'none'; 33 | } else { 34 | document.getElementById('step-1').style.display = 'none'; 35 | document.getElementById('step-2').style.display = 'block'; 36 | fillCode(); 37 | } 38 | 39 | var clipboard = new Clipboard('#code-btn'); 40 | clipboard.on('success', function (e) { 41 | e.clearSelection(); 42 | showTooltip(e.trigger, 'Copied!'); 43 | }); 44 | document 45 | .getElementById('code-btn') 46 | .addEventListener('mouseleave', function (e) { 47 | e.currentTarget.setAttribute('class', 'btn'); 48 | e.currentTarget.removeAttribute('aria-label'); 49 | }); 50 | }); 51 | 52 | function showTooltip(elem, msg) { 53 | elem.setAttribute('class', 'btn tooltipped tooltipped-s'); 54 | elem.setAttribute('aria-label', msg); 55 | } 56 | 57 | function generateCode() { 58 | var team = document.getElementById('team').value.toLowerCase(); 59 | var clientId = document.getElementById('client_id').value; 60 | var redirectUrl = encodeURIComponent(REDIRECT_URI + '?client_id=' + clientId); 61 | var scope = encodeURIComponent(SCOPE.join(' ')); 62 | var url = OAUTH_URL.replace(/__TEAM__/g, team).replace(/__CLIENT_ID__/g, clientId).replace(/__REDIRECT_URI__/g, redirectUrl).replace(/__SCOPE__/g, scope); 63 | window.open(url, '_self'); 64 | } 65 | 66 | function fillCode() { 67 | var code = getParameterByName('client_id') + '|' + getParameterByName('code'); 68 | document.getElementById('code').value = code; 69 | } 70 | 71 | function getParameterByName(name, url) { 72 | if (!url) { 73 | url = window.location.href; 74 | } 75 | name = name.replace(/[\[\]]/g, '\\$&'); 76 | var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), 77 | results = regex.exec(url); 78 | if (!results) { 79 | return null; 80 | } 81 | if (!results[2]) { 82 | return ''; 83 | } 84 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 85 | } 86 | 87 | window.alfredSlack = { 88 | generateCode: generateCode 89 | }; 90 | 91 | })(window); 92 | -------------------------------------------------------------------------------- /src/public/assets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 'Open Sans', sans-serif; 4 | } 5 | 6 | .container { 7 | background-color: #9b59b6; 8 | background-image: linear-gradient(to bottom, #8e44ad 0, #9b59b6 100%); 9 | padding: 100px 0; 10 | } 11 | 12 | h1 { 13 | color: white; 14 | font-weight: normal; 15 | font-size: 70px; 16 | text-align: center; 17 | margin-bottom: 30px; 18 | } 19 | 20 | .images { 21 | text-align: center; 22 | margin: 20px 0; 23 | } 24 | 25 | .images img { 26 | width: 80px; 27 | background-color: white; 28 | padding: 30px; 29 | border-radius: 50%; 30 | margin: 0 10px; 31 | } 32 | 33 | .images .image-alfred { 34 | padding: 36.9px 30px; 35 | } 36 | 37 | .input { 38 | text-align: center; 39 | } 40 | 41 | .input input { 42 | width: 800px; 43 | height: 60px; 44 | font-size: 1.5rem; 45 | text-align: center; 46 | border-radius: 3px; 47 | border: 1px solid #ddd; 48 | } 49 | 50 | .btn { 51 | position: relative; 52 | margin: 20px auto; 53 | display: block; 54 | padding: 15px 30px; 55 | font-size: 20px; 56 | font-weight: bold; 57 | line-height: 20px; 58 | color: white; 59 | white-space: nowrap; 60 | vertical-align: middle; 61 | cursor: pointer; 62 | background-color: transparent; 63 | border: 1px solid white; 64 | border-radius: 3px; 65 | -webkit-user-select: none; 66 | -moz-user-select: none; 67 | -ms-user-select: none; 68 | user-select: none; 69 | -webkit-appearance: none; 70 | } 71 | 72 | .btn:hover, .btn:active { 73 | text-decoration: none; 74 | background-color: white; 75 | color: #9b59b6; 76 | border-color: #ccc; 77 | } 78 | 79 | .help { 80 | text-align: center; 81 | color: white; 82 | margin: 60px 0 20px; 83 | font-size: 18px; 84 | } 85 | 86 | .github-buttons { 87 | text-align: center; 88 | transform: translateY(100%); 89 | } 90 | 91 | .step { 92 | display: none; 93 | } 94 | 95 | #step-1 input { 96 | display: block; 97 | margin: 20px auto; 98 | width: 450px; 99 | height: 50px; 100 | font-size: 1.5rem; 101 | text-align: center; 102 | border-radius: 3px; 103 | border: 1px solid #ddd; 104 | } 105 | 106 | .tooltipped { 107 | position: relative 108 | } 109 | 110 | .tooltipped:after { 111 | position: absolute; 112 | z-index: 1000000; 113 | display: none; 114 | padding: 5px 8px; 115 | font: normal normal 16px/1.5 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 116 | color: #fff; 117 | text-align: center; 118 | text-decoration: none; 119 | text-shadow: none; 120 | text-transform: none; 121 | letter-spacing: normal; 122 | word-wrap: break-word; 123 | white-space: pre; 124 | pointer-events: none; 125 | content: attr(aria-label); 126 | background: rgba(0, 0, 0, 0.8); 127 | border-radius: 3px; 128 | -webkit-font-smoothing: subpixel-antialiased 129 | } 130 | 131 | .tooltipped:before { 132 | position: absolute; 133 | z-index: 1000001; 134 | display: none; 135 | width: 0; 136 | height: 0; 137 | color: rgba(0, 0, 0, 0.8); 138 | pointer-events: none; 139 | content: ""; 140 | border: 5px solid transparent 141 | } 142 | 143 | .tooltipped:hover:before, .tooltipped:hover:after, .tooltipped:active:before, .tooltipped:active:after, .tooltipped:focus:before, .tooltipped:focus:after { 144 | display: inline-block; 145 | text-decoration: none 146 | } 147 | 148 | .tooltipped-multiline:hover:after, .tooltipped-multiline:active:after, .tooltipped-multiline:focus:after { 149 | display: table-cell 150 | } 151 | 152 | .tooltipped-s:after, .tooltipped-se:after, .tooltipped-sw:after { 153 | top: 100%; 154 | right: 50%; 155 | margin-top: 5px 156 | } 157 | 158 | .tooltipped-s:before, .tooltipped-se:before, .tooltipped-sw:before { 159 | top: auto; 160 | right: 50%; 161 | bottom: -5px; 162 | margin-right: -5px; 163 | border-bottom-color: rgba(0, 0, 0, 0.8) 164 | } 165 | 166 | .tooltipped-se:after { 167 | right: auto; 168 | left: 50%; 169 | margin-left: -15px 170 | } 171 | 172 | .tooltipped-sw:after { 173 | margin-right: -15px 174 | } 175 | 176 | .tooltipped-n:after, .tooltipped-ne:after, .tooltipped-nw:after { 177 | right: 50%; 178 | bottom: 100%; 179 | margin-bottom: 5px 180 | } 181 | 182 | .tooltipped-n:before, .tooltipped-ne:before, .tooltipped-nw:before { 183 | top: -5px; 184 | right: 50%; 185 | bottom: auto; 186 | margin-right: -5px; 187 | border-top-color: rgba(0, 0, 0, 0.8) 188 | } 189 | 190 | .tooltipped-ne:after { 191 | right: auto; 192 | left: 50%; 193 | margin-left: -15px 194 | } 195 | 196 | .tooltipped-nw:after { 197 | margin-right: -15px 198 | } 199 | 200 | .tooltipped-s:after, .tooltipped-n:after { 201 | -webkit-transform: translateX(50%); 202 | -ms-transform: translateX(50%); 203 | transform: translateX(50%) 204 | } 205 | 206 | .tooltipped-w:after { 207 | right: 100%; 208 | bottom: 50%; 209 | margin-right: 5px; 210 | -webkit-transform: translateY(50%); 211 | -ms-transform: translateY(50%); 212 | transform: translateY(50%) 213 | } 214 | 215 | .tooltipped-w:before { 216 | top: 50%; 217 | bottom: 50%; 218 | left: -5px; 219 | margin-top: -5px; 220 | border-left-color: rgba(0, 0, 0, 0.8) 221 | } 222 | 223 | .tooltipped-e:after { 224 | bottom: 50%; 225 | left: 100%; 226 | margin-left: 5px; 227 | -webkit-transform: translateY(50%); 228 | -ms-transform: translateY(50%); 229 | transform: translateY(50%) 230 | } 231 | 232 | .tooltipped-e:before { 233 | top: 50%; 234 | right: -5px; 235 | bottom: 50%; 236 | margin-top: -5px; 237 | border-right-color: rgba(0, 0, 0, 0.8) 238 | } 239 | 240 | .tooltipped-multiline:after { 241 | width: -webkit-max-content; 242 | width: -moz-max-content; 243 | width: max-content; 244 | max-width: 250px; 245 | word-break: break-word; 246 | word-wrap: normal; 247 | white-space: pre-line; 248 | border-collapse: separate 249 | } 250 | 251 | .tooltipped-multiline.tooltipped-s:after, .tooltipped-multiline.tooltipped-n:after { 252 | right: auto; 253 | left: 50%; 254 | -webkit-transform: translateX(-50%); 255 | -ms-transform: translateX(-50%); 256 | transform: translateX(-50%) 257 | } 258 | 259 | .tooltipped-multiline.tooltipped-w:after, .tooltipped-multiline.tooltipped-e:after { 260 | right: 100% 261 | } 262 | -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 |
14 |

Alfred Slack Workflow

15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 |
25 |

Copy the code below and add it to your client ID and secret.

26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 |

Made with ♥ by Yannick Galatol

34 | 39 | 44 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/scripts/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yannickglt/alfred-slack", 3 | "description": "Alfred workflow to interact with Slack", 4 | "require": { 5 | "frlnc/php-slack": "~1.0" 6 | }, 7 | "authors": [ 8 | { 9 | "name": "Yannick Galatol", 10 | "email": "yannick.galatol@gmail.com" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "AlfredSlack\\Config\\": ["config/"], 16 | "AlfredSlack\\Controllers\\": ["controllers/"], 17 | "AlfredSlack\\Helpers\\": ["helpers/"], 18 | "AlfredSlack\\Helpers\\Core\\": ["helpers/core/"], 19 | "AlfredSlack\\Helpers\\Http\\": ["helpers/http/"], 20 | "AlfredSlack\\Helpers\\Service\\": ["helpers/service/"], 21 | "AlfredSlack\\Libs\\": ["libs/"], 22 | "AlfredSlack\\Models\\": ["models/"] 23 | }, 24 | "files": ["config/routes.php"] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/scripts/composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "d43ee538370090dc4fb3e4a41b8a1639", 8 | "content-hash": "cd55de03ebee03773fab0301f2367217", 9 | "packages": [ 10 | { 11 | "name": "frlnc/php-slack", 12 | "version": "1.0.1", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/Frlnc/php-slack.git", 16 | "reference": "b73a6b6ea1282a3f70d29d7c2c77e2d3dc89c88d" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/Frlnc/php-slack/zipball/b73a6b6ea1282a3f70d29d7c2c77e2d3dc89c88d", 21 | "reference": "b73a6b6ea1282a3f70d29d7c2c77e2d3dc89c88d", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": ">=5.4.0" 26 | }, 27 | "require-dev": { 28 | "mockery/mockery": "~0.9", 29 | "phpunit/phpunit": "~4.0" 30 | }, 31 | "type": "library", 32 | "autoload": { 33 | "psr-4": { 34 | "Frlnc\\Slack\\": "src/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "Connor Parks", 44 | "email": "connor@connorvg.tv" 45 | } 46 | ], 47 | "description": "A lightweight PHP implementation of Slack's API.", 48 | "keywords": [ 49 | "slack" 50 | ], 51 | "time": "2015-04-17 18:43:15" 52 | } 53 | ], 54 | "packages-dev": [], 55 | "aliases": [], 56 | "minimum-stability": "stable", 57 | "stability-flags": [], 58 | "prefer-stable": false, 59 | "prefer-lowest": false, 60 | "platform": [], 61 | "platform-dev": [] 62 | } 63 | -------------------------------------------------------------------------------- /src/scripts/composer.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickglt/alfred-slack/197d1c2ac9e170e7e1e9312a7b06517e1594e914/src/scripts/composer.phar -------------------------------------------------------------------------------- /src/scripts/config/emojis.json: -------------------------------------------------------------------------------- 1 | { 2 | ":-1:": "👎", 3 | ":+1:": "👍", 4 | ":100:": "💯", 5 | ":1234:": "🔢", 6 | ":8ball:": "🎱", 7 | ":e-mail:": "📧", 8 | ":non-potable_water:": "🚱", 9 | ":a:": "🅰", 10 | ":ab:": "🆎", 11 | ":abc:": "🔤", 12 | ":abcd:": "🔡", 13 | ":accept:": "🉑", 14 | ":aerial_tramway:": "🚡", 15 | ":airplane:": "✈", 16 | ":alarm_clock:": "⏰", 17 | ":alien:": "👽", 18 | ":ambulance:": "🚑", 19 | ":anchor:": "⚓", 20 | ":angel:": "👼", 21 | ":anger:": "💢", 22 | ":angry:": "😠", 23 | ":anguished:": "😧", 24 | ":ant:": "🐜", 25 | ":apple:": "🍎", 26 | ":aquarius:": "♒", 27 | ":aries:": "♈", 28 | ":arrow_backward:": "◀", 29 | ":arrow_double_down:": "⏬", 30 | ":arrow_double_up:": "⏫", 31 | ":arrow_down:": "⬇", 32 | ":arrow_down_small:": "🔽", 33 | ":arrow_forward:": "▶", 34 | ":arrow_heading_down:": "⤵", 35 | ":arrow_heading_up:": "⤴", 36 | ":arrow_left:": "⬅", 37 | ":arrow_lower_left:": "↙", 38 | ":arrow_lower_right:": "↘", 39 | ":arrow_right:": "➡", 40 | ":arrow_right_hook:": "↪", 41 | ":arrow_up:": "⬆", 42 | ":arrow_up_down:": "↕", 43 | ":arrow_up_small:": "🔼", 44 | ":arrow_upper_left:": "↖", 45 | ":arrow_upper_right:": "↗", 46 | ":arrows_clockwise:": "🔃", 47 | ":arrows_counterclockwise:": "🔄", 48 | ":art:": "🎨", 49 | ":articulated_lorry:": "🚛", 50 | ":astonished:": "😲", 51 | ":athletic_shoe:": "👟", 52 | ":atm:": "🏧", 53 | ":b:": "🅱", 54 | ":baby:": "👶", 55 | ":baby_bottle:": "🍼", 56 | ":baby_chick:": "🐤", 57 | ":baby_symbol:": "🚼", 58 | ":back:": "🔙", 59 | ":baggage_claim:": "🛄", 60 | ":balloon:": "🎈", 61 | ":ballot_box_with_check:": "☑", 62 | ":bamboo:": "🎍", 63 | ":banana:": "🍌", 64 | ":bangbang:": "‼", 65 | ":bank:": "🏦", 66 | ":bar_chart:": "📊", 67 | ":barber:": "💈", 68 | ":baseball:": "⚾", 69 | ":basketball:": "🏀", 70 | ":bath:": "🛀", 71 | ":bathtub:": "🛁", 72 | ":battery:": "🔋", 73 | ":bear:": "🐻", 74 | ":bee:": "🐝", 75 | ":beer:": "🍺", 76 | ":beers:": "🍻", 77 | ":beetle:": "🐞", 78 | ":beginner:": "🔰", 79 | ":bell:": "🔔", 80 | ":bento:": "🍱", 81 | ":bicyclist:": "🚴", 82 | ":bike:": "🚲", 83 | ":bikini:": "👙", 84 | ":bird:": "🐦", 85 | ":birthday:": "🎂", 86 | ":black_circle:": "⚫", 87 | ":black_joker:": "🃏", 88 | ":black_large_square:": "⬛", 89 | ":black_medium_small_square:": "◾", 90 | ":black_medium_square:": "◼", 91 | ":black_nib:": "✒", 92 | ":black_small_square:": "▪", 93 | ":black_square_button:": "🔲", 94 | ":blossom:": "🌼", 95 | ":blowfish:": "🐡", 96 | ":blue_book:": "📘", 97 | ":blue_car:": "🚙", 98 | ":blue_heart:": "💙", 99 | ":blush:": "😊", 100 | ":boar:": "🐗", 101 | ":boat:": "⛵", 102 | ":bomb:": "💣", 103 | ":book:": "📖", 104 | ":bookmark:": "🔖", 105 | ":bookmark_tabs:": "📑", 106 | ":books:": "📚", 107 | ":boom:": "💥", 108 | ":boot:": "👢", 109 | ":bouquet:": "💐", 110 | ":bow:": "🙇", 111 | ":bowling:": "🎳", 112 | ":boy:": "👦", 113 | ":bread:": "🍞", 114 | ":bride_with_veil:": "👰", 115 | ":bridge_at_night:": "🌉", 116 | ":briefcase:": "💼", 117 | ":broken_heart:": "💔", 118 | ":bug:": "🐛", 119 | ":bulb:": "💡", 120 | ":bullettrain_front:": "🚅", 121 | ":bullettrain_side:": "🚄", 122 | ":bus:": "🚌", 123 | ":busstop:": "🚏", 124 | ":bust_in_silhouette:": "👤", 125 | ":busts_in_silhouette:": "👥", 126 | ":cactus:": "🌵", 127 | ":cake:": "🍰", 128 | ":calendar:": "📆", 129 | ":calling:": "📲", 130 | ":camel:": "🐫", 131 | ":camera:": "📷", 132 | ":cancer:": "♋", 133 | ":candy:": "🍬", 134 | ":capital_abcd:": "🔠", 135 | ":capricorn:": "♑", 136 | ":car:": "🚗", 137 | ":card_index:": "📇", 138 | ":carousel_horse:": "🎠", 139 | ":cat2:": "🐈", 140 | ":cat:": "🐱", 141 | ":cd:": "💿", 142 | ":chart:": "💹", 143 | ":chart_with_downwards_trend:": "📉", 144 | ":chart_with_upwards_trend:": "📈", 145 | ":checkered_flag:": "🏁", 146 | ":cherries:": "🍒", 147 | ":cherry_blossom:": "🌸", 148 | ":chestnut:": "🌰", 149 | ":chicken:": "🐔", 150 | ":children_crossing:": "🚸", 151 | ":chocolate_bar:": "🍫", 152 | ":christmas_tree:": "🎄", 153 | ":church:": "⛪", 154 | ":cinema:": "🎦", 155 | ":circus_tent:": "🎪", 156 | ":city_sunrise:": "🌇", 157 | ":city_sunset:": "🌆", 158 | ":cl:": "🆑", 159 | ":clap:": "👏", 160 | ":clapper:": "🎬", 161 | ":clipboard:": "📋", 162 | ":clock1030:": "🕥", 163 | ":clock10:": "🕙", 164 | ":clock1130:": "🕦", 165 | ":clock11:": "🕚", 166 | ":clock1230:": "🕧", 167 | ":clock12:": "🕛", 168 | ":clock130:": "🕜", 169 | ":clock1:": "🕐", 170 | ":clock230:": "🕝", 171 | ":clock2:": "🕑", 172 | ":clock330:": "🕞", 173 | ":clock3:": "🕒", 174 | ":clock430:": "🕟", 175 | ":clock4:": "🕓", 176 | ":clock530:": "🕠", 177 | ":clock5:": "🕔", 178 | ":clock630:": "🕡", 179 | ":clock6:": "🕕", 180 | ":clock730:": "🕢", 181 | ":clock7:": "🕖", 182 | ":clock830:": "🕣", 183 | ":clock8:": "🕗", 184 | ":clock930:": "🕤", 185 | ":clock9:": "🕘", 186 | ":closed_book:": "📕", 187 | ":closed_lock_with_key:": "🔐", 188 | ":closed_umbrella:": "🌂", 189 | ":cloud:": "☁", 190 | ":clubs:": "♣", 191 | ":cn:": "🇨🇳", 192 | ":cocktail:": "🍸", 193 | ":coffee:": "☕", 194 | ":cold_sweat:": "😰", 195 | ":collision:": "💥", 196 | ":computer:": "💻", 197 | ":confetti_ball:": "🎊", 198 | ":confounded:": "😖", 199 | ":confused:": "😕", 200 | ":congratulations:": "㊗", 201 | ":construction:": "🚧", 202 | ":construction_worker:": "👷", 203 | ":convenience_store:": "🏪", 204 | ":cookie:": "🍪", 205 | ":cool:": "🆒", 206 | ":cop:": "👮", 207 | ":copyright:": "©", 208 | ":corn:": "🌽", 209 | ":couple:": "👫", 210 | ":couple_with_heart:": "💑", 211 | ":couplekiss:": "💏", 212 | ":cow2:": "🐄", 213 | ":cow:": "🐮", 214 | ":credit_card:": "💳", 215 | ":crescent_moon:": "🌙", 216 | ":crocodile:": "🐊", 217 | ":crossed_flags:": "🎌", 218 | ":crown:": "👑", 219 | ":cry:": "😢", 220 | ":crying_cat_face:": "😿", 221 | ":crystal_ball:": "🔮", 222 | ":cupid:": "💘", 223 | ":curly_loop:": "➰", 224 | ":currency_exchange:": "💱", 225 | ":curry:": "🍛", 226 | ":custard:": "🍮", 227 | ":customs:": "🛃", 228 | ":cyclone:": "🌀", 229 | ":dancer:": "💃", 230 | ":dancers:": "👯", 231 | ":dango:": "🍡", 232 | ":dart:": "🎯", 233 | ":dash:": "💨", 234 | ":date:": "📅", 235 | ":de:": "🇩🇪", 236 | ":deciduous_tree:": "🌳", 237 | ":department_store:": "🏬", 238 | ":diamond_shape_with_a_dot_inside:": "💠", 239 | ":diamonds:": "♦", 240 | ":disappointed:": "😞", 241 | ":disappointed_relieved:": "😥", 242 | ":dizzy:": "💫", 243 | ":dizzy_face:": "😵", 244 | ":do_not_litter:": "🚯", 245 | ":dog2:": "🐕", 246 | ":dog:": "🐶", 247 | ":dollar:": "💵", 248 | ":dolls:": "🎎", 249 | ":dolphin:": "🐬", 250 | ":door:": "🚪", 251 | ":doughnut:": "🍩", 252 | ":dragon:": "🐉", 253 | ":dragon_face:": "🐲", 254 | ":dress:": "👗", 255 | ":dromedary_camel:": "🐪", 256 | ":droplet:": "💧", 257 | ":dvd:": "📀", 258 | ":ear:": "👂", 259 | ":ear_of_rice:": "🌾", 260 | ":earth_africa:": "🌍", 261 | ":earth_americas:": "🌎", 262 | ":earth_asia:": "🌏", 263 | ":egg:": "🍳", 264 | ":eggplant:": "🍆", 265 | ":eight:": "8", 266 | ":eight_pointed_black_star:": "✴", 267 | ":eight_spoked_asterisk:": "✳", 268 | ":electric_plug:": "🔌", 269 | ":elephant:": "🐘", 270 | ":email:": "✉", 271 | ":end:": "🔚", 272 | ":envelope:": "✉", 273 | ":es:": "🇪🇸", 274 | ":euro:": "💶", 275 | ":european_castle:": "🏰", 276 | ":european_post_office:": "🏤", 277 | ":evergreen_tree:": "🌲", 278 | ":exclamation:": "❗", 279 | ":expressionless:": "😑", 280 | ":eyeglasses:": "👓", 281 | ":eyes:": "👀", 282 | ":facepunch:": "👊", 283 | ":factory:": "🏭", 284 | ":fallen_leaf:": "🍂", 285 | ":family:": "👪", 286 | ":fast_forward:": "⏩", 287 | ":fax:": "📠", 288 | ":fearful:": "😨", 289 | ":feet:": "👣", 290 | ":ferris_wheel:": "🎡", 291 | ":file_folder:": "📁", 292 | ":fire:": "🔥", 293 | ":fire_engine:": "🚒", 294 | ":fireworks:": "🎆", 295 | ":first_quarter_moon:": "🌓", 296 | ":first_quarter_moon_with_face:": "🌛", 297 | ":fish:": "🐟", 298 | ":fish_cake:": "🍥", 299 | ":fishing_pole_and_fish:": "🎣", 300 | ":fist:": "✊", 301 | ":five:": "5", 302 | ":flags:": "🎏", 303 | ":flashlight:": "🔦", 304 | ":floppy_disk:": "💾", 305 | ":flower_playing_cards:": "🎴", 306 | ":flushed:": "😳", 307 | ":foggy:": "🌁", 308 | ":football:": "🏈", 309 | ":footprints:": "👣", 310 | ":fork_and_knife:": "🍴", 311 | ":fountain:": "⛲", 312 | ":four:": "4", 313 | ":four_leaf_clover:": "🍀", 314 | ":fr:": "🇫🇷", 315 | ":free:": "🆓", 316 | ":fried_shrimp:": "🍤", 317 | ":fries:": "🍟", 318 | ":frog:": "🐸", 319 | ":frowning:": "😦", 320 | ":fu:": "🖕", 321 | ":fuelpump:": "⛽", 322 | ":full_moon:": "🌕", 323 | ":full_moon_with_face:": "🌝", 324 | ":game_die:": "🎲", 325 | ":gb:": "🇬🇧", 326 | ":gem:": "💎", 327 | ":gemini:": "♊", 328 | ":ghost:": "👻", 329 | ":gift:": "🎁", 330 | ":gift_heart:": "💝", 331 | ":girl:": "👧", 332 | ":globe_with_meridians:": "🌐", 333 | ":goat:": "🐐", 334 | ":golf:": "⛳", 335 | ":grapes:": "🍇", 336 | ":green_apple:": "🍏", 337 | ":green_book:": "📗", 338 | ":green_heart:": "💚", 339 | ":grey_exclamation:": "❕", 340 | ":grey_question:": "❔", 341 | ":grimacing:": "😬", 342 | ":grin:": "😁", 343 | ":grinning:": "😀", 344 | ":guardsman:": "💂", 345 | ":guitar:": "🎸", 346 | ":gun:": "🔫", 347 | ":haircut:": "💇", 348 | ":hamburger:": "🍔", 349 | ":hammer:": "🔨", 350 | ":hamster:": "🐹", 351 | ":hand:": "✋", 352 | ":handbag:": "👜", 353 | ":hankey:": "💩", 354 | ":hash:": "#", 355 | ":hatched_chick:": "🐥", 356 | ":hatching_chick:": "🐣", 357 | ":headphones:": "🎧", 358 | ":hear_no_evil:": "🙉", 359 | ":heart:": "❤️", 360 | ":heart_decoration:": "💟", 361 | ":heart_eyes:": "😍", 362 | ":heart_eyes_cat:": "😻", 363 | ":heartbeat:": "💓", 364 | ":heartpulse:": "💗", 365 | ":hearts:": "♥", 366 | ":heavy_check_mark:": "✔️", 367 | ":heavy_division_sign:": "➗", 368 | ":heavy_dollar_sign:": "💲", 369 | ":heavy_exclamation_mark:": "❗", 370 | ":heavy_minus_sign:": "➖", 371 | ":heavy_multiplication_x:": "✖️", 372 | ":heavy_plus_sign:": "➕", 373 | ":helicopter:": "🚁", 374 | ":herb:": "🌿", 375 | ":hibiscus:": "🌺", 376 | ":high_brightness:": "🔆", 377 | ":high_heel:": "👠", 378 | ":hocho:": "🔪", 379 | ":honey_pot:": "🍯", 380 | ":honeybee:": "🐝", 381 | ":horse:": "🐴", 382 | ":horse_racing:": "🏇", 383 | ":hospital:": "🏥", 384 | ":hotel:": "🏨", 385 | ":hotsprings:": "♨", 386 | ":hourglass:": "⌛", 387 | ":hourglass_flowing_sand:": "⏳", 388 | ":house:": "🏠", 389 | ":house_with_garden:": "🏡", 390 | ":hushed:": "😯", 391 | ":ice_cream:": "🍨", 392 | ":icecream:": "🍦", 393 | ":id:": "🆔", 394 | ":ideograph_advantage:": "🉐", 395 | ":imp:": "👿", 396 | ":inbox_tray:": "📥", 397 | ":incoming_envelope:": "📨", 398 | ":information_desk_person:": "💁", 399 | ":information_source:": "ℹ", 400 | ":innocent:": "😇", 401 | ":interrobang:": "⁉", 402 | ":iphone:": "📱", 403 | ":it:": "🇮🇹", 404 | ":izakaya_lantern:": "🏮", 405 | ":jack_o_lantern:": "🎃", 406 | ":japan:": "🗾", 407 | ":japanese_castle:": "🏯", 408 | ":japanese_goblin:": "👺", 409 | ":japanese_ogre:": "👹", 410 | ":jeans:": "👖", 411 | ":joy:": "😂", 412 | ":joy_cat:": "😹", 413 | ":jp:": "🇯🇵", 414 | ":key:": "🔑", 415 | ":keycap_ten:": "🔟", 416 | ":kimono:": "👘", 417 | ":kiss:": "💋", 418 | ":kissing:": "😗", 419 | ":kissing_cat:": "😽", 420 | ":kissing_closed_eyes:": "😚", 421 | ":kissing_heart:": "😘", 422 | ":kissing_smiling_eyes:": "😙", 423 | ":koala:": "🐨", 424 | ":koko:": "🈁", 425 | ":kr:": "🇰🇷", 426 | ":lantern:": "🏮", 427 | ":large_blue_circle:": "🔵", 428 | ":large_blue_diamond:": "🔷", 429 | ":large_orange_diamond:": "🔶", 430 | ":last_quarter_moon:": "🌗", 431 | ":last_quarter_moon_with_face:": "🌜", 432 | ":laughing:": "😆", 433 | ":leaves:": "🍃", 434 | ":ledger:": "📒", 435 | ":left_luggage:": "🛅", 436 | ":left_right_arrow:": "↔", 437 | ":leftwards_arrow_with_hook:": "↩", 438 | ":lemon:": "🍋", 439 | ":leo:": "♌", 440 | ":leopard:": "🐆", 441 | ":libra:": "♎", 442 | ":light_rail:": "🚈", 443 | ":link:": "🔗", 444 | ":lips:": "👄", 445 | ":lipstick:": "💄", 446 | ":lock:": "🔒", 447 | ":lock_with_ink_pen:": "🔏", 448 | ":lollipop:": "🍭", 449 | ":loop:": "➿", 450 | ":loudspeaker:": "📢", 451 | ":love_hotel:": "🏩", 452 | ":love_letter:": "💌", 453 | ":low_brightness:": "🔅", 454 | ":m:": "Ⓜ", 455 | ":mag:": "🔍", 456 | ":mag_right:": "🔎", 457 | ":mahjong:": "🀄", 458 | ":mailbox:": "📫", 459 | ":mailbox_closed:": "📪", 460 | ":mailbox_with_mail:": "📬", 461 | ":mailbox_with_no_mail:": "📭", 462 | ":man:": "👨", 463 | ":man_with_gua_pi_mao:": "👲", 464 | ":man_with_turban:": "👳", 465 | ":mans_shoe:": "👞", 466 | ":maple_leaf:": "🍁", 467 | ":mask:": "😷", 468 | ":massage:": "💆", 469 | ":meat_on_bone:": "🍖", 470 | ":mega:": "📣", 471 | ":melon:": "🍈", 472 | ":memo:": "📝", 473 | ":mens:": "🚹", 474 | ":metro:": "🚇", 475 | ":microphone:": "🎤", 476 | ":microscope:": "🔬", 477 | ":milky_way:": "🌌", 478 | ":minibus:": "🚐", 479 | ":minidisc:": "💽", 480 | ":mobile_phone_off:": "📴", 481 | ":money_with_wings:": "💸", 482 | ":moneybag:": "💰", 483 | ":monkey:": "🐒", 484 | ":monkey_face:": "🐵", 485 | ":monorail:": "🚝", 486 | ":moon:": "🌙", 487 | ":mortar_board:": "🎓", 488 | ":mount_fuji:": "🗻", 489 | ":mountain_bicyclist:": "🚵", 490 | ":mountain_cableway:": "🚠", 491 | ":mountain_railway:": "🚞", 492 | ":mouse2:": "🐁", 493 | ":mouse:": "🐭", 494 | ":movie_camera:": "🎥", 495 | ":moyai:": "🗿", 496 | ":muscle:": "💪", 497 | ":mushroom:": "🍄", 498 | ":musical_keyboard:": "🎹", 499 | ":musical_note:": "🎵", 500 | ":musical_score:": "🎼", 501 | ":mute:": "🔇", 502 | ":nail_care:": "💅", 503 | ":name_badge:": "📛", 504 | ":necktie:": "👔", 505 | ":negative_squared_cross_mark:": "❎", 506 | ":neutral_face:": "😐", 507 | ":new:": "🆕", 508 | ":new_moon:": "🌑", 509 | ":new_moon_with_face:": "🌚", 510 | ":newspaper:": "📰", 511 | ":ng:": "🆖", 512 | ":nine:": "9", 513 | ":no_bell:": "🔕", 514 | ":no_bicycles:": "🚳", 515 | ":no_entry:": "⛔", 516 | ":no_entry_sign:": "🚫", 517 | ":no_good:": "🙅", 518 | ":no_mobile_phones:": "📵", 519 | ":no_mouth:": "😶", 520 | ":no_pedestrians:": "🚷", 521 | ":no_smoking:": "🚭", 522 | ":nose:": "👃", 523 | ":notebook:": "📓", 524 | ":notebook_with_decorative_cover:": "📔", 525 | ":notes:": "🎶", 526 | ":nut_and_bolt:": "🔩", 527 | ":o2:": "🅾", 528 | ":o:": "⭕", 529 | ":ocean:": "🌊", 530 | ":octopus:": "🐙", 531 | ":oden:": "🍢", 532 | ":office:": "🏢", 533 | ":ok:": "🆗", 534 | ":ok_hand:": "👌", 535 | ":ok_woman:": "🙆", 536 | ":older_man:": "👴", 537 | ":older_woman:": "👵", 538 | ":on:": "🔛", 539 | ":oncoming_automobile:": "🚘", 540 | ":oncoming_bus:": "🚍", 541 | ":oncoming_police_car:": "🚔", 542 | ":oncoming_taxi:": "🚖", 543 | ":one:": "1", 544 | ":open_file_folder:": "📂", 545 | ":open_book:": "📖", 546 | ":open_hands:": "👐", 547 | ":open_mouth:": "😮", 548 | ":ophiuchus:": "⛎", 549 | ":orange_book:": "📙", 550 | ":outbox_tray:": "📤", 551 | ":ox:": "🐂", 552 | ":package:": "📦", 553 | ":page_facing_up:": "📄", 554 | ":page_with_curl:": "📃", 555 | ":pager:": "📟", 556 | ":palm_tree:": "🌴", 557 | ":panda_face:": "🐼", 558 | ":paperclip:": "📎", 559 | ":parking:": "🅿", 560 | ":part_alternation_mark:": "〽", 561 | ":partly_sunny:": "⛅", 562 | ":passport_control:": "🛂", 563 | ":paw_prints:": "🐾", 564 | ":peach:": "🍑", 565 | ":pear:": "🍐", 566 | ":pencil2:": "✏", 567 | ":penguin:": "🐧", 568 | ":pensive:": "😔", 569 | ":performing_arts:": "🎭", 570 | ":persevere:": "😣", 571 | ":person_frowning:": "🙍", 572 | ":person_with_blond_hair:": "👱", 573 | ":person_with_pouting_face:": "🙎", 574 | ":phone:": "☎", 575 | ":pig2:": "🐖", 576 | ":pig:": "🐷", 577 | ":pig_nose:": "🐽", 578 | ":pill:": "💊", 579 | ":pineapple:": "🍍", 580 | ":pisces:": "♓", 581 | ":pizza:": "🍕", 582 | ":point_down:": "👇", 583 | ":point_left:": "👈", 584 | ":point_right:": "👉", 585 | ":point_up:": "☝", 586 | ":point_up_2:": "👆", 587 | ":police_car:": "🚓", 588 | ":poodle:": "🐩", 589 | ":poop:": "💩", 590 | ":post_office:": "🏣", 591 | ":postal_horn:": "📯", 592 | ":postbox:": "📮", 593 | ":potable_water:": "🚰", 594 | ":pouch:": "👝", 595 | ":poultry_leg:": "🍗", 596 | ":pound:": "💷", 597 | ":pouting_cat:": "😾", 598 | ":pray:": "🙏", 599 | ":princess:": "👸", 600 | ":punch:": "👊", 601 | ":purple_heart:": "💜", 602 | ":purse:": "👛", 603 | ":pushpin:": "📌", 604 | ":put_litter_in_its_place:": "🚮", 605 | ":question:": "❓", 606 | ":rabbit2:": "🐇", 607 | ":rabbit:": "🐰", 608 | ":racehorse:": "🐎", 609 | ":radio:": "📻", 610 | ":radio_button:": "🔘", 611 | ":rage:": "😡", 612 | ":railway_car:": "🚃", 613 | ":rainbow:": "🌈", 614 | ":raised_hand:": "🙋", 615 | ":raising_hand:": "🙋", 616 | ":raised_hands:": "🙌", 617 | ":ram:": "🐏", 618 | ":ramen:": "🍜", 619 | ":rat:": "🐀", 620 | ":recycle:": "♻", 621 | ":red_car:": "🚗", 622 | ":red_circle:": "🔴", 623 | ":registered:": "®", 624 | ":relaxed:": "☺", 625 | ":relieved:": "😌", 626 | ":repeat:": "🔁", 627 | ":repeat_one:": "🔂", 628 | ":restroom:": "🚻", 629 | ":revolving_hearts:": "💞", 630 | ":rewind:": "⏪", 631 | ":ribbon:": "🎀", 632 | ":rice:": "🍚", 633 | ":rice_ball:": "🍙", 634 | ":rice_cracker:": "🍘", 635 | ":rice_scene:": "🎑", 636 | ":ring:": "💍", 637 | ":robot_face:": "🤖", 638 | ":rocket:": "🚀", 639 | ":roller_coaster:": "🎢", 640 | ":rooster:": "🐓", 641 | ":rose:": "🌹", 642 | ":rotating_light:": "🚨", 643 | ":round_pushpin:": "📍", 644 | ":rowboat:": "🚣", 645 | ":ru:": "🇷🇺", 646 | ":rugby_football:": "🏉", 647 | ":runner:": "🏃", 648 | ":running:": "🏃", 649 | ":running_shirt_with_sash:": "🎽", 650 | ":sa:": "🈂", 651 | ":sagittarius:": "♐", 652 | ":sailboat:": "⛵", 653 | ":sake:": "🍶", 654 | ":sandal:": "👡", 655 | ":santa:": "🎅", 656 | ":satellite:": "📡", 657 | ":satisfied:": "😌", 658 | ":saxophone:": "🎷", 659 | ":school:": "🏫", 660 | ":school_satchel:": "🎒", 661 | ":scissors:": "✂", 662 | ":scorpius:": "♏", 663 | ":scream:": "😱", 664 | ":scream_cat:": "🙀", 665 | ":scroll:": "📜", 666 | ":seat:": "💺", 667 | ":secret:": "㊙", 668 | ":see_no_evil:": "🙈", 669 | ":seedling:": "🌱", 670 | ":seven:": "7", 671 | ":shaved_ice:": "🍧", 672 | ":sheep:": "🐑", 673 | ":shell:": "🐚", 674 | ":ship:": "🚢", 675 | ":shirt:": "👕", 676 | ":shit:": "💩", 677 | ":shoe:": "👞", 678 | ":shower:": "🚿", 679 | ":signal_strength:": "📶", 680 | ":six:": "6", 681 | ":six_pointed_star:": "🔯", 682 | ":ski:": "🎿", 683 | ":skin-tone-2:": "🏻", 684 | ":skin-tone-3:": "🏼", 685 | ":skin-tone-4:": "🏽", 686 | ":skin-tone-5:": "🏾", 687 | ":skin-tone-6:": "🏿", 688 | ":skull:": "💀", 689 | ":sleeping:": "😴", 690 | ":sleepy:": "😪", 691 | ":slightly_smiling_face:": "🙂", 692 | ":slot_machine:": "🎰", 693 | ":small_blue_diamond:": "🔹", 694 | ":small_orange_diamond:": "🔸", 695 | ":small_red_triangle:": "🔺", 696 | ":small_red_triangle_down:": "🔻", 697 | ":smile:": "😄", 698 | ":smile_cat:": "😸", 699 | ":smiley:": "😃", 700 | ":smiley_cat:": "😺", 701 | ":smiling_imp:": "😈", 702 | ":smirk:": "😏", 703 | ":smirk_cat:": "😼", 704 | ":smoking:": "🚬", 705 | ":snail:": "🐌", 706 | ":snake:": "🐍", 707 | ":snowboarder:": "🏂", 708 | ":snowflake:": "❄", 709 | ":snowman:": "⛄", 710 | ":sob:": "😭", 711 | ":soccer:": "⚽", 712 | ":soon:": "🔜", 713 | ":sos:": "🆘", 714 | ":sound:": "🔉", 715 | ":space_invader:": "👾", 716 | ":spades:": "♠", 717 | ":spaghetti:": "🍝", 718 | ":sparkle:": "❇", 719 | ":sparkler:": "🎇", 720 | ":sparkles:": "✨", 721 | ":sparkling_heart:": "💖", 722 | ":speak_no_evil:": "🙊", 723 | ":speaker:": "🔊", 724 | ":speech_balloon:": "💬", 725 | ":speedboat:": "🚤", 726 | ":spock:": "🖖", 727 | ":star:": "⭐", 728 | ":star2:": "🌟", 729 | ":stars:": "🌃", 730 | ":station:": "🚉", 731 | ":statue_of_liberty:": "🗽", 732 | ":steam_locomotive:": "🚂", 733 | ":stew:": "🍲", 734 | ":straight_ruler:": "📏", 735 | ":strawberry:": "🍓", 736 | ":stuck_out_tongue:": "😛", 737 | ":stuck_out_tongue_closed_eyes:": "😝", 738 | ":stuck_out_tongue_winking_eye:": "😜", 739 | ":sun_with_face:": "🌞", 740 | ":sunflower:": "🌻", 741 | ":sunglasses:": "😎", 742 | ":sunny:": "☀", 743 | ":sunrise:": "🌅", 744 | ":sunrise_over_mountains:": "🌄", 745 | ":surfer:": "🏄", 746 | ":sushi:": "🍣", 747 | ":suspension_railway:": "🚟", 748 | ":sweat:": "😓", 749 | ":sweat_drops:": "💦", 750 | ":sweat_smile:": "😅", 751 | ":sweet_potato:": "🍠", 752 | ":swimmer:": "🏊", 753 | ":symbols:": "🔣", 754 | ":syringe:": "💉", 755 | ":tada:": "🎉", 756 | ":tanabata_tree:": "🎋", 757 | ":tangerine:": "🍊", 758 | ":taurus:": "♉", 759 | ":taxi:": "🚕", 760 | ":tea:": "🍵", 761 | ":telephone:": "☎", 762 | ":telephone_receiver:": "📞", 763 | ":telescope:": "🔭", 764 | ":tennis:": "🎾", 765 | ":tent:": "⛺", 766 | ":thinking_face:": "🤔", 767 | ":thought_balloon:": "💭", 768 | ":three:": "3", 769 | ":thumbsdown:": "👎", 770 | ":thumbsup:": "👍", 771 | ":ticket:": "🎫", 772 | ":tiger2:": "🐅", 773 | ":tiger:": "🐯", 774 | ":tired_face:": "😫", 775 | ":tm:": "™", 776 | ":toilet:": "🚽", 777 | ":tokyo_tower:": "🗼", 778 | ":tomato:": "🍅", 779 | ":tongue:": "👅", 780 | ":top:": "🔝", 781 | ":tophat:": "🎩", 782 | ":tractor:": "🚜", 783 | ":traffic_light:": "🚥", 784 | ":train:": "🚃", 785 | ":train2:": "🚆", 786 | ":tram:": "🚊", 787 | ":triangular_flag_on_post:": "🚩", 788 | ":triangular_ruler:": "📐", 789 | ":trident:": "🔱", 790 | ":triumph:": "😤", 791 | ":trolleybus:": "🚎", 792 | ":trophy:": "🏆", 793 | ":tropical_drink:": "🍹", 794 | ":tropical_fish:": "🐠", 795 | ":truck:": "🚚", 796 | ":trumpet:": "🎺", 797 | ":tshirt:": "👕", 798 | ":tulip:": "🌷", 799 | ":turtle:": "🐢", 800 | ":tv:": "📺", 801 | ":twisted_rightwards_arrows:": "🔀", 802 | ":two:": "2", 803 | ":two_hearts:": "💕", 804 | ":two_men_holding_hands:": "👬", 805 | ":two_women_holding_hands:": "👭", 806 | ":u5272:": "🈹", 807 | ":u5408:": "🈴", 808 | ":u55b6:": "🈺", 809 | ":u6307:": "🈯", 810 | ":u6708:": "🈷", 811 | ":u6709:": "🈶", 812 | ":u6e80:": "🈵", 813 | ":u7121:": "🈚", 814 | ":u7533:": "🈸", 815 | ":u7981:": "🈲", 816 | ":u7a7a:": "🈳", 817 | ":uk:": "🇬🇧", 818 | ":umbrella:": "☔", 819 | ":unamused:": "😒", 820 | ":underage:": "🔞", 821 | ":unlock:": "🔓", 822 | ":up:": "🆙", 823 | ":us:": "🇺🇸", 824 | ":v:": "✌", 825 | ":vertical_traffic_light:": "🚦", 826 | ":vhs:": "📼", 827 | ":vibration_mode:": "📳", 828 | ":video_camera:": "📹", 829 | ":video_game:": "🎮", 830 | ":violin:": "🎻", 831 | ":virgo:": "♍", 832 | ":volcano:": "🌋", 833 | ":vs:": "🆚", 834 | ":walking:": "🚶", 835 | ":waning_crescent_moon:": "🌘", 836 | ":waning_gibbous_moon:": "🌖", 837 | ":warning:": "⚠", 838 | ":watch:": "⌚", 839 | ":water_buffalo:": "🐃", 840 | ":watermelon:": "🍉", 841 | ":wave:": "👋", 842 | ":wavy_dash:": "〰", 843 | ":waxing_crescent_moon:": "🌒", 844 | ":waxing_gibbous_moon:": "🌔", 845 | ":wc:": "🚾", 846 | ":weary:": "😩", 847 | ":wedding:": "💒", 848 | ":whale2:": "🐋", 849 | ":whale:": "🐳", 850 | ":wheelchair:": "♿", 851 | ":white_check_mark:": "✅", 852 | ":white_circle:": "⚪", 853 | ":white_flower:": "💮", 854 | ":white_large_square:": "⬜", 855 | ":white_medium_small_square:": "◽", 856 | ":white_medium_square:": "◻", 857 | ":white_small_square:": "▫", 858 | ":white_square_button:": "🔳", 859 | ":wind_chime:": "🎐", 860 | ":wine_glass:": "🍷", 861 | ":wink:": "😉", 862 | ":wolf:": "🐺", 863 | ":woman:": "👩", 864 | ":womans_clothes:": "👚", 865 | ":womans_hat:": "👒", 866 | ":womens:": "🚺", 867 | ":worried:": "😟", 868 | ":wrench:": "🔧", 869 | ":x:": "❌", 870 | ":yellow_heart:": "💛", 871 | ":yen:": "💴", 872 | ":yum:": "😋", 873 | ":zap:": "⚡", 874 | ":zero:": "0", 875 | ":zzz:": "💤" 876 | } 877 | -------------------------------------------------------------------------------- /src/scripts/config/routes.php: -------------------------------------------------------------------------------- 1 | route('--files :search?', [ 11 | 'controller' => 'config', 12 | 'action' => 'getFiles' 13 | ]) 14 | ->route('--stars :search?', [ 15 | 'controller' => 'config', 16 | 'action' => 'getStarredItems' 17 | ]) 18 | ->route('--search :search', [ 19 | 'controller' => 'config', 20 | 'action' => 'search' 21 | ]) 22 | ->route('--presence :presence?', [ 23 | 'controller' => 'config', 24 | 'action' => 'listPresences' 25 | ]) 26 | ->route('--status :search?', [ 27 | 'controller' => 'config', 28 | 'action' => 'getStatuses' 29 | ]) 30 | ->route('--remove-client :search?', [ 31 | 'controller' => 'config', 32 | 'action' => 'getTeams' 33 | ]) 34 | ->route('--:config :param', [ 35 | 'controller' => 'config', 36 | 'action' => 'listConfigs' 37 | ]) 38 | ->route('--:config?', [ 39 | 'controller' => 'config', 40 | 'action' => 'listConfigs' 41 | ]) 42 | ->route(':channel :message', [ 43 | 'controller' => 'channel', 44 | 'action' => 'getChannels' 45 | ]) 46 | ->route(':channel ', [ 47 | 'controller' => 'channel', 48 | 'action' => 'getChannelHistory' 49 | ]) 50 | ->route(':channel', [ 51 | 'controller' => 'channel', 52 | 'action' => 'getChannels' 53 | ]); 54 | }); 55 | -------------------------------------------------------------------------------- /src/scripts/controllers/ChannelController.php: -------------------------------------------------------------------------------- 1 | service->getChannels(true); 15 | foreach ($channels as $channel) { 16 | $results[] = [ 17 | 'title' => '#' . $channel->getName(), 18 | 'description' => $channel . '', 19 | 'autocomplete' => '#' . $channel->getName() . ' ', 20 | 'route' => new Route('channel', 'openChannel', ['channel' => $channel]) 21 | ]; 22 | } 23 | 24 | $users = $this->getUsers(); 25 | foreach ($users as $user) { 26 | $icon = $this->service->getProfileIcon($user->getId()); 27 | $results[] = [ 28 | 'title' => '@' . $user->getName(), 29 | 'description' => $user . '', 30 | 'icon' => $icon, 31 | 'autocomplete' => '@' . $user->getName() . ' ', 32 | 'route' => new Route('channel', 'openChannel', ['channel' => $user]) 33 | ]; 34 | } 35 | 36 | $this->results = $this->deduplicateChannels($results); 37 | 38 | $this->results = $this->filterResults($this->results, $search); 39 | 40 | if (!empty($message) && (count($this->results) > 0)) { 41 | $firstResult = $this->results[0]; 42 | $firstResult['title'] = 'Send "' . $message . '" to ' . $firstResult['title']; 43 | $firstResult['autocomplete'] .= $message; 44 | $firstResult['route'] = new Route('channel', 'sendMessage', ['channel' => $firstResult['route']->getParams()['channel'], 'message' => $message]); 45 | $this->results = [$firstResult]; 46 | } 47 | 48 | $this->render(); 49 | } 50 | 51 | public function getChannelHistoryAction($search) { 52 | 53 | $results = []; 54 | 55 | $channels = $this->service->getChannels(true); 56 | foreach ($channels as $channel) { 57 | $results[] = [ 58 | 'id' => $channel->getId(), 59 | 'title' => '#' . $channel->getName(), 60 | 'description' => $channel . '', 61 | 'autocomplete' => '#' . $channel->getName() . ' ', 62 | 'route' => new Route('channel', 'openChannel', ['channel' => $channel]) 63 | ]; 64 | } 65 | 66 | $users = $this->getUsers(); 67 | foreach ($users as $user) { 68 | $results[] = [ 69 | 'id' => $user->getId(), 70 | 'title' => '@' . $user->getName(), 71 | 'description' => $user . '', 72 | 'autocomplete' => '@' . $user->getName() . ' ', 73 | 'route' => new Route('channel', 'openChannel', ['channel' => $user]) 74 | ]; 75 | } 76 | 77 | $results = $this->deduplicateChannels($results); 78 | 79 | $results = $this->filterResults($results, $search); 80 | 81 | if (count($results) === 0) { 82 | return; 83 | } 84 | 85 | $history = []; 86 | $firstResult = $results[0]; 87 | $data = $firstResult['route']->getParams()['channel']; 88 | $icon = null; 89 | if ($data instanceof \AlfredSlack\Models\ChannelModel) { 90 | $history = $this->service->getChannelHistory($data); 91 | } elseif ($data instanceof \AlfredSlack\Models\GroupModel) { 92 | $history = $this->service->getGroupHistory($data); 93 | $this->service->markGroupAsRead($data); 94 | } elseif ($data instanceof \AlfredSlack\Models\UserModel) { 95 | $im = $this->service->getImByUser($data); 96 | $history = $this->service->getImHistory($im); 97 | $icon = $this->service->getProfileIcon($data->getId()); 98 | $this->service->markImAsRead($im); 99 | } 100 | 101 | if (empty($history)) { 102 | $this->results[] = [ 103 | 'title' => 'No history', 104 | 'icon' => $icon, 105 | 'autocomplete' => $firstResult['title'] . ' ', 106 | 'route' => $firstResult['route'] 107 | ]; 108 | } else { 109 | foreach ($history as $message) { 110 | $date = new \DateTime(); 111 | $date->setTimestamp($message->getTs()); 112 | $this->results[] = [ 113 | 'title' => $message->getText(), 114 | 'description' => $date->format('F jS - H:i'), 115 | 'icon' => $icon, 116 | 'autocomplete' => $firstResult['title'] . ' ', 117 | 'route' => $firstResult['route'] 118 | ]; 119 | } 120 | } 121 | 122 | $this->render(false); 123 | } 124 | 125 | public function openChannelAction(\AlfredSlack\Models\ChatInterface $channel) { 126 | 127 | $id = $channel->getId(); 128 | 129 | $url = 'slack://channel?id=' . $id . '&team=' . $channel->getAuth()->getTeamId(); 130 | 131 | Utils::openUrl($url); 132 | Utils::openApp('Slack'); 133 | } 134 | 135 | public function sendMessageAction($channel, $message) { 136 | 137 | $this->service->postMessage($channel, $message); 138 | 139 | // Get the IM id if a user 140 | $title = $channel->getName(); 141 | if ($channel instanceof \AlfredSlack\Models\UserModel) { 142 | $title = '@' . $title; 143 | } else { 144 | $title = '#' . $title; 145 | } 146 | 147 | $this->notify('Message sent successfully to ' . $title); 148 | } 149 | 150 | private function getUsers($excludeSlackBot = false) { 151 | $users = $this->service->getUsers(true); 152 | if ($excludeSlackBot !== false) { 153 | $users = Utils::filter($users, function ($user) { 154 | return $user->getId() !== $user->getAuth()->user_id; 155 | }); 156 | } else { 157 | $meInTeams = Utils::filter($users, function ($user) { 158 | return $user->getId() === $user->getAuth()->user_id; 159 | }); 160 | foreach ($meInTeams as $me) { 161 | $me->getProfile()->real_name = "{$me->getProfile()->real_name} (you)"; 162 | } 163 | } 164 | return $users; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/scripts/controllers/ConfigController.php: -------------------------------------------------------------------------------- 1 | '--add-client', 18 | 'description' => 'Add a Slack client', 19 | 'autocomplete' => '--add-client ', 20 | 'route' => new Route('config', 'saveClient', ['clientCredentials' => $param]) 21 | ], 22 | [ 23 | 'title' => '--remove-client', 24 | 'description' => 'Remove a Slack client', 25 | 'autocomplete' => '--remove-client ', 26 | 'route' => new Route('config', 'removeClient', ['team' => $param]) 27 | ], 28 | [ 29 | 'title' => '--add-token', 30 | 'description' => 'Add a Slack token in the keychain (deprecated)', 31 | 'autocomplete' => '--add-token ', 32 | 'route' => new Route('config', 'saveToken', ['token' => $param]) 33 | ], 34 | [ 35 | 'title' => '--add-token-unsafe', 36 | 'description' => 'Add a Slack token in the cache instead of the keychain (deprecated)', 37 | 'autocomplete' => '--add-token-unsafe ', 38 | 'route' => new Route('config', 'saveTokenUnsafe', ['token' => $param]) 39 | ], 40 | [ 41 | 'title' => '--mark', 42 | 'description' => 'Mark all as read', 43 | 'autocomplete' => '--mark ', 44 | 'route' => new Route('config', 'markAllAsRead') 45 | ], 46 | [ 47 | 'title' => '--files', 48 | 'description' => 'List the files within the team', 49 | 'autocomplete' => '--files ', 50 | 'route' => new Route('config', 'getFiles', ['search' => $param]) 51 | ], 52 | [ 53 | 'title' => '--search', 54 | 'description' => 'Search both messages and files', 55 | 'autocomplete' => '--search ', 56 | 'route' => new Route('config', 'search', ['search' => $param]) 57 | ], 58 | [ 59 | 'title' => '--stars', 60 | 'description' => 'List the items starred', 61 | 'autocomplete' => '--stars ', 62 | 'route' => new Route('config', 'getStarredItems', ['search' => $param]) 63 | ], 64 | [ 65 | 'title' => '--status', 66 | 'description' => 'List the custom statuses', 67 | 'autocomplete' => '--status ', 68 | 'route' => new Route('config', 'getStatuses', ['status' => $param]) 69 | ], 70 | [ 71 | 'title' => '--presence', 72 | 'description' => 'Set the user presence (either active or away)', 73 | 'autocomplete' => '--presence ', 74 | 'route' => new Route('config', 'listPresences', ['presence' => $param]) 75 | ], 76 | [ 77 | 'title' => '--refresh', 78 | 'description' => 'Refresh the cache', 79 | 'autocomplete' => '--refresh ', 80 | 'route' => new Route('config', 'refreshCache') 81 | ] 82 | ]; 83 | 84 | if (!empty($action)) { 85 | $this->results = $this->filterResults($results, $action); 86 | } else { 87 | $this->results = $results; 88 | } 89 | 90 | $this->render(); 91 | } 92 | 93 | public function listPresencesAction($presence) { 94 | 95 | $results = [ 96 | [ 97 | 'title' => '--presence away', 98 | 'description' => 'Set the presence as away', 99 | 'icon' => 'images/circle_grey.png', 100 | 'autocomplete' => '--presence away ', 101 | 'route' => new Route('config', 'setPresence', ['presence' => 'away']) 102 | ], 103 | [ 104 | 'title' => '--presence active', 105 | 'description' => 'Set the presence as active', 106 | 'icon' => 'images/circle_green.png', 107 | 'autocomplete' => '--presence active ', 108 | 'route' => new Route('config', 'setPresence', ['presence' => 'active']) 109 | ], 110 | ]; 111 | 112 | if (empty($presence)) { 113 | $this->results = $results; 114 | } else { 115 | $this->results = $this->filterResults($results, $presence); 116 | } 117 | 118 | $this->render(); 119 | } 120 | 121 | public function getFilesAction($search) { 122 | $files = $this->service->getFiles(); 123 | 124 | $results = []; 125 | foreach ($files as $file) { 126 | $icon = !is_null($file->getThumb64()) ? $this->service->getFileIcon($file->getId()) : null; 127 | $results[] = [ 128 | 'id' => $file->getId(), 129 | 'title' => $file->getName(), 130 | 'description' => $file->getTitle(), 131 | 'icon' => $icon, 132 | 'autocomplete' => '--files ' . $file->getName(), 133 | 'route' => new Route('config', 'openFile', ['file' => $file]) 134 | ]; 135 | } 136 | 137 | if (empty($search)) { 138 | $this->results = $results; 139 | } else { 140 | $this->results = $this->filterResults($results, $search); 141 | } 142 | 143 | $this->render(false); 144 | } 145 | 146 | public function getTeamsAction($search) { 147 | $teams = $this->service->getTeams(); 148 | 149 | $results = []; 150 | foreach ($teams as $team) { 151 | $results[] = [ 152 | 'id' => $team->team_id, 153 | 'title' => $team->team, 154 | 'description' => 'Remove the Slack client "'.$team->team.'"', 155 | 'autocomplete' => '--remove-client ' . $team->team, 156 | 'route' => new Route('config', 'removeClient', ['team' => $team->team]) 157 | ]; 158 | } 159 | 160 | if (empty($search)) { 161 | $this->results = $results; 162 | } else { 163 | $this->results = $this->filterResults($results, $search); 164 | } 165 | 166 | $this->render(false); 167 | } 168 | 169 | public function getStarredItemsAction($search) { 170 | $items = $this->service->getStarredItems(); 171 | 172 | $results = []; 173 | foreach ($items as $item) { 174 | if ($item instanceof \AlfredSlack\Models\MessageModel) { 175 | $date = new \DateTime(); 176 | $date->setTimestamp($item->getTs()); 177 | $results[] = [ 178 | 'title' => $item->getText(), 179 | 'description' => $date->format('F jS - H:i'), 180 | 'icon' => $this->service->getProfileIcon($item->getUser()), 181 | 'autocomplete' => '--stars ' . $item->getText(), 182 | 'route' => new Route('config', 'openFile', ['file' => $item]) // open the message like a file (redirect to the slack history) 183 | ]; 184 | } elseif ($item instanceof \AlfredSlack\Models\FileModel) { 185 | $icon = !is_null($item->getThumb64()) ? $this->service->getFileIcon($item->getId()) : null; 186 | $results[] = [ 187 | 'id' => $item->getId(), 188 | 'title' => $item->getName(), 189 | 'description' => $item->getTitle(), 190 | 'icon' => $icon, 191 | 'autocomplete' => '--stars ' . $item->getName(), 192 | 'route' => new Route('config', 'openFile', ['file' => $item]) 193 | ]; 194 | } 195 | } 196 | 197 | if (empty($search)) { 198 | $this->results = $results; 199 | } else { 200 | $this->results = $this->filterResults($results, $search); 201 | } 202 | 203 | $this->render(false); 204 | } 205 | 206 | public function getStatusesAction($status) { 207 | $results = [ 208 | [ 209 | 'title' => '--status in a meeting', 210 | 'description' => 'Set the custom status as "In a meeting" - 1 hour', 211 | 'icon' => 'images/spiral_calendar.png', 212 | 'autocomplete' => '--status meeting ', 213 | 'route' => new Route('config', 'setStatus', ['status' => 'meeting']) 214 | ], 215 | [ 216 | 'title' => '--status commuting', 217 | 'description' => 'Set the custom status as "Commuting" - 30 minutes', 218 | 'icon' => 'images/bus.png', 219 | 'autocomplete' => '--status commuting ', 220 | 'route' => new Route('config', 'setStatus', ['status' => 'commuting']) 221 | ], 222 | [ 223 | 'title' => '--status out sick', 224 | 'description' => 'Set the custom status as "Out sick" - Today', 225 | 'icon' => 'images/face_with_thermometer.png', 226 | 'autocomplete' => '--status sick ', 227 | 'route' => new Route('config', 'setStatus', ['status' => 'sick']) 228 | ], 229 | [ 230 | 'title' => '--status vacationing', 231 | 'description' => 'Set the custom status as "Vacationing" - Don\'t clear', 232 | 'icon' => 'images/palm_tree.png', 233 | 'autocomplete' => '--status vacationing ', 234 | 'route' => new Route('config', 'setStatus', ['status' => 'vacationing']) 235 | ], 236 | [ 237 | 'title' => '--status working remotely', 238 | 'description' => 'Set the custom status as "Working remotely" - Today', 239 | 'icon' => 'images/house_with_garden.png', 240 | 'autocomplete' => '--status remote ', 241 | 'route' => new Route('config', 'setStatus', ['status' => 'remote']) 242 | ], 243 | [ 244 | 'title' => '--status clear', 245 | 'description' => 'Clear the current custom status', 246 | 'icon' => 'images/cross_mark.png', 247 | 'autocomplete' => '--status clear ', 248 | 'route' => new Route('config', 'setStatus', ['status' => 'clear']) 249 | ] 250 | ]; 251 | 252 | if (empty($status)) { 253 | $this->results = $results; 254 | } else { 255 | $this->results = $this->filterResults($results, $status); 256 | } 257 | 258 | $this->render(); 259 | } 260 | 261 | public function searchAction($query) { 262 | $items = $this->service->search($query); 263 | 264 | $results = []; 265 | foreach ($items as $item) { 266 | if ($item instanceof \AlfredSlack\Models\MessageModel) { 267 | $date = new \DateTime(); 268 | $date->setTimestamp($item->getTs()); 269 | $results[] = [ 270 | 'title' => $item->getText(), 271 | 'description' => $date->format('F jS - H:i'), 272 | 'icon' => $this->service->getProfileIcon($item->getUser()), 273 | 'route' => new Route('config', 'openFile', ['file' => $item]) // open the message like a file (redirect to the slack history) 274 | ]; 275 | } elseif ($item instanceof \AlfredSlack\Models\FileModel) { 276 | $icon = !is_null($item->getThumb64()) ? $this->service->getFileIcon($item->getId()) : null; 277 | $results[] = [ 278 | 'id' => $item->getId(), 279 | 'title' => $item->getName(), 280 | 'description' => $item->getTitle(), 281 | 'icon' => $icon, 282 | 'route' => new Route('config', 'openFile', ['file' => $item]) 283 | ]; 284 | } 285 | } 286 | $this->results = $results; 287 | 288 | $this->render(false); 289 | } 290 | 291 | public function openFileAction($file) { 292 | Utils::openUrl($file->getPermalink()); 293 | } 294 | 295 | public function saveClientAction($clientCredentials) { 296 | try { 297 | $this->service->addClient($clientCredentials); 298 | $this->notify('Client saved successfully'); 299 | } catch (\Exception $e) { 300 | $this->notify($e->getMessage()); 301 | } 302 | } 303 | 304 | public function removeClientAction($team) { 305 | try { 306 | $this->service->removeTeam($team); 307 | $this->notify('Client removed successfully'); 308 | } catch (\Exception $e) { 309 | $this->notify($e->getMessage()); 310 | } 311 | } 312 | 313 | public function saveTokenAction($token) { 314 | $this->service->addToken($token); 315 | $this->notify('Token saved successfully'); 316 | } 317 | 318 | public function saveTokenUnsafeAction($token) { 319 | $this->service->addTokenUnsafe($token); 320 | $this->notify('Token saved successfully'); 321 | } 322 | 323 | public function setPresenceAction($presence) { 324 | $isAway = (strtolower($presence) === 'away'); 325 | $this->service->setPresence($isAway); 326 | $this->notify('You are now marked as ​"' . ($isAway ? 'away' : 'active') . '"'); 327 | } 328 | 329 | public function setStatusAction($statusName) { 330 | $status = new StatusModel([ 331 | 'status_text' => '', 332 | 'status_emoji' => '', 333 | 'status_expiration' => 0 334 | ]); 335 | switch ($statusName) { 336 | case 'meeting': 337 | $status = new StatusModel([ 338 | 'status_text' => 'In a meeting', 339 | 'status_emoji' => ':spiral_calendar_pad:', 340 | 'status_expiration' => time() + 3600 341 | ]); 342 | break; 343 | case 'commuting': 344 | $status = new StatusModel([ 345 | 'status_text' => 'Commuting', 346 | 'status_emoji' => ':bus:', 347 | 'status_expiration' => time() + 1800 348 | ]); 349 | break; 350 | case 'sick': 351 | $status = new StatusModel([ 352 | 'status_text' => 'Out sick', 353 | 'status_emoji' => ':face_with_thermometer:', 354 | 'status_expiration' => strtotime(date('Y-m-d 23:59:59', time())) 355 | ]); 356 | break; 357 | case 'vacationing': 358 | $status = new StatusModel([ 359 | 'status_text' => 'Vacationing', 360 | 'status_emoji' => ':palm_tree:', 361 | 'status_expiration' => 0 362 | ]); 363 | break; 364 | case 'remote': 365 | $status = new StatusModel([ 366 | 'status_text' => 'Working remotely', 367 | 'status_emoji' => ':house_with_garden:', 368 | 'status_expiration' => strtotime(date('Y-m-d 23:59:59', time())) 369 | ]); 370 | break; 371 | } 372 | 373 | $this->service->setStatus($status); 374 | 375 | if (empty($status->status_text)) { 376 | $this->notify('Your custom status was cleared'); 377 | } else { 378 | $this->notify('You are now marked as "' . $status->status_text . '"'); 379 | } 380 | } 381 | 382 | public function refreshCacheAction() { 383 | $this->service->setCacheLock(true); 384 | try { 385 | $this->service->refreshCache(); 386 | $this->notify('Cache refresh successfully'); 387 | } catch (Exception $e) { 388 | $this->notify('An error occured during refresh'); 389 | } 390 | $this->service->setCacheLock(false); 391 | } 392 | 393 | public function markAllAsReadAction() { 394 | $this->service->markAllAsRead(); 395 | } 396 | 397 | } 398 | -------------------------------------------------------------------------------- /src/scripts/controllers/Controller.php: -------------------------------------------------------------------------------- 1 | workflows = Utils::getWorkflows(); 16 | $this->results = []; 17 | } 18 | 19 | public function preDispatch($action, $params) { 20 | 21 | } 22 | 23 | public function dispatch($action, $params) { 24 | return call_user_func_array(array($this, $action), $params); 25 | } 26 | 27 | public function postDispatch($action, $params, $result) { 28 | 29 | } 30 | 31 | protected function render($sorted = true) { 32 | if ($this->isNotified) { 33 | throw new Exception('Cannot use the method "render" if the method "notify" was called'); 34 | } 35 | 36 | $i = 0; 37 | foreach ($this->results as $result) { 38 | $uid = $sorted ? $i++ : null; 39 | $icon = isset($result['icon']) ? $result['icon'] : Utils::$icon; 40 | $route = isset($result['route']) ? json_encode($result['route']) : null; 41 | $autocomplete = isset($result['autocomplete']) ? $result['autocomplete'] : null; 42 | $description = isset($result['description']) ? $result['description'] : null; 43 | $this->workflows->result($uid, $route, $result['title'], $description, $icon, 'yes', $autocomplete); 44 | } 45 | 46 | $this->isRendered = true; 47 | 48 | echo $this->workflows->toxml(); 49 | } 50 | 51 | protected function notify($message) { 52 | if ($this->isRendered) { 53 | throw new Exception('Cannot use the method "render" if the method "notify" was called'); 54 | } 55 | 56 | $this->isNotified = true; 57 | 58 | echo $message; 59 | } 60 | 61 | protected function deduplicateChannels(array $results) { 62 | $resultsByAutocomplete = Utils::groupBy($results, 'autocomplete'); 63 | $resultsByAutocomplete = Utils::filter($resultsByAutocomplete, function ($v) { 64 | return count($v) > 1; 65 | }); 66 | 67 | $autocompletes = []; 68 | foreach ($resultsByAutocomplete as $res) { 69 | foreach ($res as $result) { 70 | $autocompletes[] = $result['autocomplete']; 71 | } 72 | } 73 | 74 | foreach ($results as &$result) { 75 | if (in_array($result['autocomplete'], $autocompletes)) { 76 | $channel = $result['route']->getParams()['channel']; 77 | $autocomplete = $channel->getAuth()->getTeam(); 78 | $autocomplete .= ($channel instanceof \AlfredSlack\Models\UserModel) ? '@' : '#'; 79 | $autocomplete .= $channel->getName() . ' '; 80 | $result['autocomplete'] = $autocomplete; 81 | } 82 | } 83 | 84 | return $results; 85 | } 86 | 87 | protected function filterResults(array $array, $search) { 88 | $search = strtolower(Utils::deburr(trim($search))); 89 | 90 | $found = []; 91 | $results = []; 92 | foreach ($array as $id => $element) { 93 | 94 | $title = strtolower(Utils::deburr(trim($element['title']))); 95 | $autocomplete = strtolower(Utils::deburr(trim($element['autocomplete']))); 96 | $description = strtolower(Utils::deburr(trim($element['description']))); 97 | 98 | if ($autocomplete === $search) { 99 | if (!isset($found[$id])) { 100 | $found[$id] = true; 101 | $results[0][] = $element; 102 | } 103 | } else if ($title === $search) { 104 | if (!isset($found[$id])) { 105 | $found[$id] = true; 106 | $results[0][] = $element; 107 | } 108 | } else if (strpos($autocomplete, $search) === 0) { 109 | if (!isset($found[$id])) { 110 | $found[$id] = true; 111 | $results[1][] = $element; 112 | } 113 | } else if (strpos($title, $search) === 0) { 114 | if (!isset($found[$id])) { 115 | $found[$id] = true; 116 | $results[1][] = $element; 117 | } 118 | } else if (strpos($title, $search) > 0) { 119 | if (!isset($found[$id])) { 120 | $found[$id] = true; 121 | $element['__searchIndex'] = strpos($title, $search); 122 | $results[2][] = $element; 123 | } 124 | } else if (strpos($autocomplete, $search) > 0) { 125 | if (!isset($found[$id])) { 126 | $found[$id] = true; 127 | $element['__searchIndex'] = strpos($autocomplete, $search); 128 | $results[2][] = $element; 129 | } 130 | } else if (strpos($description, $search) !== false) { 131 | if (!isset($found[$id])) { 132 | $found[$id] = true; 133 | $element['__searchIndex'] = strpos($description, $search); 134 | $results[3][] = $element; 135 | } 136 | } 137 | } 138 | 139 | if (isset($results[2])) { 140 | usort($results[2], function ($a, $b) { 141 | if ($a['__searchIndex'] === $b['__searchIndex']) { 142 | $al = strlen($a['title']); 143 | $bl = strlen($b['title']); 144 | if ($al === $bl) { 145 | return 0; 146 | } 147 | return ($al < $bl) ? -1 : 1; 148 | } 149 | return ($a['__searchIndex'] < $b['__searchIndex']) ? -1 : 1; 150 | }); 151 | } 152 | 153 | if (isset($results[3])) { 154 | usort($results[3], function ($a, $b) { 155 | if ($a['__searchIndex'] === $b['__searchIndex']) { 156 | $al = strlen($a['description']); 157 | $bl = strlen($b['description']); 158 | if ($al === $bl) { 159 | return 0; 160 | } 161 | return ($al < $bl) ? -1 : 1; 162 | } 163 | return ($a['__searchIndex'] < $b['__searchIndex']) ? -1 : 1; 164 | }); 165 | } 166 | 167 | ksort($results); 168 | 169 | $return = []; 170 | foreach ($results as $resultPerLevel) { 171 | $return = array_merge($return, $resultPerLevel); 172 | } 173 | return $return; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/scripts/controllers/SlackController.php: -------------------------------------------------------------------------------- 1 | service = new MultiTeamSlackService(); 16 | } 17 | 18 | public function preDispatch($action, $params) { 19 | 20 | // Interrupt the action if the cache is currently refreshing 21 | if ($this->service->isCacheLocked()) { 22 | $this->results = [ 23 | [ 24 | 'id' => '', 25 | 'title' => 'Refresh still in progress', 26 | 'description' => 'Please wait the end of cache refresh...' 27 | ] 28 | ]; 29 | $this->render(); 30 | return false; 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/scripts/helpers/core/CustomCommander.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'token' => true, 18 | 'endpoint' => '/users.profile.set' 19 | ] 20 | ]); 21 | } 22 | 23 | parent::__construct($token, $interactor); 24 | } 25 | 26 | /** 27 | * Executes a command. 28 | * 29 | * @param string $command 30 | * @param array $parameters 31 | * @return \Frlnc\Slack\Contracts\Http\Response 32 | */ 33 | public function execute($command, array $parameters = []) { 34 | if (!isset(static::$commands[$command])) 35 | throw new InvalidArgumentException("The command '{$command}' is not currently supported"); 36 | 37 | $command = static::$commands[$command]; 38 | 39 | if ($command['token']) 40 | $parameters = array_merge($parameters, ['token' => $this->token]); 41 | 42 | if (isset($command['format'])) 43 | foreach ($command['format'] as $format) 44 | if (isset($parameters[$format])) 45 | $parameters[$format] = static::format($parameters[$format]); 46 | 47 | $headers = []; 48 | if (isset($command['headers'])) 49 | $headers = $command['headers']; 50 | 51 | $url = static::$baseUrl . $command['endpoint']; 52 | 53 | if (isset($command['post']) && $command['post']) 54 | return $this->interactor->post($url, [], $parameters, $headers); 55 | 56 | return $this->interactor->get($url, $parameters, $headers); 57 | } 58 | 59 | /** 60 | * Executes a command. 61 | * 62 | * @param string $command 63 | * @param array $parameters 64 | * @return \Frlnc\Slack\Contracts\Http\Response 65 | */ 66 | public function executeAll($commandsWithParameters) { 67 | $requests = []; 68 | foreach ($commandsWithParameters as $commandWithParameters) { 69 | $command = $commandWithParameters['command']; 70 | if (!empty($commandWithParameters['parameters'])) { 71 | $parameters = $commandWithParameters['parameters']; 72 | } else { 73 | $parameters = []; 74 | 75 | } 76 | if (!isset(static::$commands[$command])) 77 | throw new InvalidArgumentException("The command '{$command}' is not currently supported"); 78 | 79 | $command = static::$commands[$command]; 80 | 81 | if ($command['token']) 82 | $parameters = array_merge($parameters, ['token' => $this->token]); 83 | 84 | if (isset($command['format'])) 85 | foreach ($command['format'] as $format) 86 | if (isset($parameters[$format])) 87 | $parameters[$format] = static::format($parameters[$format]); 88 | 89 | $headers = []; 90 | if (isset($command['headers'])) 91 | $headers = $command['headers']; 92 | 93 | $url = static::$baseUrl . $command['endpoint']; 94 | 95 | if (isset($command['post']) && $command['post']) 96 | return $this->interactor->post($url, [], $parameters, $headers); 97 | 98 | $requests[] = ['url' => $url, 'parameters' => $parameters, 'headers' => $headers]; 99 | } 100 | 101 | return $this->interactor->getAll($requests); 102 | } 103 | 104 | /** 105 | * Executes a command. 106 | * 107 | * @param string $command 108 | * @param array $parameters 109 | * @return \Frlnc\Slack\Contracts\Http\Response 110 | */ 111 | public function executeAsync($command, array $parameters = []) { 112 | if (!isset(static::$commands[$command])) { 113 | throw new InvalidArgumentException("The command '{$command}' is not currently supported"); 114 | } 115 | 116 | $command = static::$commands[$command]; 117 | 118 | if ($command['token']) { 119 | $parameters = array_merge($parameters, ['token' => $this->token]); 120 | } 121 | 122 | if (isset($command['format'])) { 123 | foreach ($command['format'] as $format) { 124 | if (isset($parameters[$format])) { 125 | $parameters[$format] = static::format($parameters[$format]); 126 | } 127 | } 128 | } 129 | 130 | $headers = []; 131 | if (isset($command['headers'])) { 132 | $headers = $command['headers']; 133 | } 134 | 135 | $url = static::$baseUrl . $command['endpoint']; 136 | 137 | if (isset($command['post']) && $command['post']) { 138 | $this->interactor->postAsync($url, [], $parameters, $headers); 139 | } else { 140 | $this->interactor->getAsync($url, $parameters, $headers); 141 | } 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/scripts/helpers/http/MultiCurlInteractor.php: -------------------------------------------------------------------------------- 1 | prepareRequests($urlsWithParameters); 15 | return $this->executeMultiRequest($requests['multiRequest'], $requests['singleRequests']); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function getAsync($url, array $parameters = [], array $headers = []) { 22 | $request = $this->prepareAsyncRequest($url, $parameters, $headers); 23 | $this->executeAsyncRequest($request); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function postAsync($url, array $urlParameters = [], array $postParameters = [], array $headers = []) { 30 | $request = $this->prepareAsyncRequest($url, $urlParameters, $headers); 31 | 32 | curl_setopt($request, CURLOPT_POST, count($postParameters)); 33 | curl_setopt($request, CURLOPT_POSTFIELDS, http_build_query($postParameters)); 34 | 35 | $this->executeAsyncRequest($request); 36 | } 37 | 38 | /** 39 | * Prepares a request using curl. 40 | * 41 | * @param string $url [description] 42 | * @param array $parameters [description] 43 | * @param array $headers [description] 44 | * @return resource 45 | */ 46 | protected function prepareRequests($urlsWithParameters) { 47 | $multiRequest = curl_multi_init(); 48 | $singleRequest = []; 49 | foreach ($urlsWithParameters as $urlWithParameters) { 50 | $singleRequest = curl_init(); 51 | 52 | $url = $urlWithParameters['url']; 53 | if ($query = http_build_query($urlWithParameters['parameters'])) { 54 | $url .= '?' . $query; 55 | } 56 | curl_setopt($singleRequest, CURLOPT_URL, $url); 57 | curl_setopt($singleRequest, CURLOPT_RETURNTRANSFER, true); 58 | curl_setopt($singleRequest, CURLOPT_HTTPHEADER, $urlWithParameters['headers']); 59 | curl_setopt($singleRequest, CURLINFO_HEADER_OUT, true); 60 | curl_setopt($singleRequest, CURLOPT_SSL_VERIFYPEER, false); 61 | 62 | curl_multi_add_handle($multiRequest, $singleRequest); 63 | 64 | $singleRequests[] = $singleRequest; 65 | } 66 | 67 | return ['multiRequest' => $multiRequest, 'singleRequests' => $singleRequests]; 68 | } 69 | 70 | /** 71 | * Executes a curl request. 72 | * 73 | * @param resource $request 74 | * @return \Frlnc\Slack\Contracts\Http\Response 75 | */ 76 | public function executeMultiRequest($multiRequest, $singleRequests) { 77 | $responses = []; 78 | $infos = []; 79 | $active = null; 80 | do { 81 | $status = curl_multi_exec($multiRequest, $active); 82 | $infos[] = curl_multi_info_read($multiRequest); 83 | } while ($status === CURLM_CALL_MULTI_PERFORM || $active); 84 | 85 | foreach ($singleRequests as $index => $singleRequest) { 86 | $body = curl_multi_getcontent($singleRequest); 87 | curl_multi_remove_handle($multiRequest, $singleRequest); 88 | curl_close($singleRequest); 89 | 90 | $info = $infos[$index]; 91 | 92 | $statusCode = $info['http_code']; 93 | $headers = $info['request_header']; 94 | 95 | if (function_exists('http_parse_headers')) { 96 | $headers = http_parse_headers($headers); 97 | } else { 98 | $header_text = substr($headers, 0, strpos($headers, "\r\n\r\n")); 99 | $headers = []; 100 | 101 | foreach (explode("\r\n", $header_text) as $i => $line) { 102 | if ($i !== 0) { 103 | list ($key, $value) = explode(': ', $line); 104 | $headers[$key] = $value; 105 | } 106 | } 107 | } 108 | 109 | $responses[] = $this->factory->build($body, $headers, $statusCode); 110 | } 111 | 112 | curl_multi_close($multiRequest); 113 | 114 | return $responses; 115 | } 116 | 117 | /** 118 | * Prepares a request using curl. 119 | * 120 | * @param string $url [description] 121 | * @param array $parameters [description] 122 | * @param array $headers [description] 123 | * @return resource 124 | */ 125 | protected static function prepareAsyncRequest($url, $parameters = [], $headers = []) { 126 | $multiRequest = curl_multi_init(); 127 | $singleRequest = curl_init(); 128 | 129 | if ($query = http_build_query($parameters)) { 130 | $url .= '?' . $query; 131 | } 132 | curl_setopt($singleRequest, CURLOPT_URL, $url); 133 | curl_setopt($singleRequest, CURLOPT_RETURNTRANSFER, true); 134 | curl_setopt($singleRequest, CURLOPT_HTTPHEADER, $headers); 135 | curl_setopt($singleRequest, CURLINFO_HEADER_OUT, true); 136 | curl_setopt($singleRequest, CURLOPT_SSL_VERIFYPEER, false); 137 | 138 | curl_multi_add_handle($multiRequest, $singleRequest); 139 | 140 | return $multiRequest; 141 | } 142 | 143 | /** 144 | * Executes a curl request. 145 | * 146 | * @param resource $request 147 | * @return \Frlnc\Slack\Contracts\Http\Response 148 | */ 149 | public function executeAsyncRequest($request) { 150 | $active = null; 151 | do { 152 | //ob_start(); 153 | $status = curl_multi_exec($request, $active); 154 | //ob_end_clean(); 155 | } while ($status === CURLM_CALL_MULTI_PERFORM || $active); 156 | 157 | curl_multi_close($request); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/scripts/helpers/service/MultiTeamSlackService.php: -------------------------------------------------------------------------------- 1 | initServices(); 20 | } 21 | 22 | private function initServices() { 23 | $teams = $this->getTeams(); 24 | if ($teams !== false) { 25 | foreach ($teams as $team) { 26 | $this->services[$team->team_id] = new SingleTeamSlackService($team->team_id); 27 | } 28 | } else { 29 | $oldToken = Utils::getWorkflows()->getPassword('token'); 30 | Utils::getWorkflows()->delete('token'); 31 | $this->addToken($oldToken); 32 | $this->refreshCache(); 33 | } 34 | } 35 | 36 | public function setCacheLock($lock) { 37 | if ($lock === true) { 38 | Utils::getWorkflows()->write('1', 'cache.lock'); 39 | } else { 40 | Utils::getWorkflows()->delete('cache.lock'); 41 | } 42 | } 43 | 44 | public function isCacheLocked() { 45 | return (Utils::getWorkflows()->read('cache.lock') === 1); 46 | } 47 | 48 | public function getTeams() { 49 | $teams = Utils::getWorkflows()->read('teams'); 50 | if (!is_array($teams)) { 51 | return []; 52 | } else { 53 | return Utils::filter($teams, function ($team) { 54 | return !empty($team->team_id); 55 | }); 56 | } 57 | } 58 | 59 | public function addTeam($team) { 60 | $teams = Utils::getWorkflows()->read('teams'); 61 | if ($teams === false) { 62 | $teams = []; 63 | } 64 | if (!empty($team['team']) && !empty($team['team_id']) && is_null(Utils::find($teams, ['team_id' => $team['team_id']]))) { 65 | $teams[] = ['team' => $team['team'], 'team_id' => $team['team_id']]; 66 | Utils::getWorkflows()->write($teams, 'teams'); 67 | } 68 | } 69 | 70 | public function removeTeam($teamName) { 71 | $teams = Utils::getWorkflows()->read('teams'); 72 | if ($teams === false) { 73 | $teams = []; 74 | } 75 | $formattedTeamName = strtolower($teamName); 76 | $teams = array_filter($teams, function ($team) use ($formattedTeamName) { 77 | return strtolower($team->team) !== $formattedTeamName; 78 | }); 79 | Utils::getWorkflows()->write($teams, 'teams'); 80 | } 81 | 82 | public function addClient($clientCredentials) { 83 | list($clientSecret, $clientId, $code) = $this->parseClientCredentials($clientCredentials); 84 | 85 | $interactor = new MultiCurlInteractor; 86 | $interactor->setResponseFactory(new SlackResponseFactory); 87 | $tempCommander = new CustomCommander(false, $interactor); 88 | $access = $tempCommander->execute('oauth.access', ['client_id' => $clientId, 'client_secret' => $clientSecret, 'code' => $code, 'redirect_uri' => static::$redirectUri . '?client_id=' . $clientId])->getBody(); 89 | Utils::debug('access: ' . json_encode($access)); 90 | 91 | if ($access['ok'] === false) { 92 | $message = $this->getErrorMessage($access['error']); 93 | throw new \Exception($message); 94 | } else { 95 | $token = $access['access_token']; 96 | $tempCommander->setToken($token); 97 | $auth = $tempCommander->execute('auth.test')->getBody(); 98 | $isClientCredentialsSaved = Utils::getWorkflows()->setPassword('clientCredentials.' . $auth['team_id'], $clientCredentials); 99 | $isTokenSaved = Utils::getWorkflows()->setPassword('token.' . $auth['team_id'], $token); 100 | if ($isClientCredentialsSaved && $isTokenSaved) { 101 | $this->addTeam($auth); 102 | // If safe password is set, remove the unsafe one 103 | Utils::getWorkflows()->delete('token.' . $auth['team_id']); 104 | $this->services[$auth['team_id']] = new SingleTeamSlackService($auth['team_id']); 105 | } 106 | } 107 | } 108 | 109 | public function addToken($token) { 110 | if (empty($token)) { 111 | return; 112 | } 113 | 114 | $interactor = new MultiCurlInteractor; 115 | $interactor->setResponseFactory(new SlackResponseFactory); 116 | $tempCommander = new CustomCommander($token, $interactor); 117 | $auth = $tempCommander->execute('auth.test')->getBody(); 118 | 119 | if (Utils::getWorkflows()->setPassword('token.' . $auth['team_id'], $token)) { 120 | $this->addTeam($auth); 121 | // If safe password is set, remove the unsafe one 122 | Utils::getWorkflows()->delete('token.' . $auth['team_id']); 123 | $this->services[$auth['team_id']] = new SingleTeamSlackService($auth['team_id']); 124 | } 125 | } 126 | 127 | public function addTokenUnsafe($token) { 128 | } 129 | 130 | public function getProfileIcon($userId) { 131 | foreach ($this->services as $model) { 132 | $icon = $model->getProfileIcon($userId); 133 | if ($icon !== false) { 134 | return $icon; 135 | } 136 | } 137 | return false; 138 | } 139 | 140 | public function getFileIcon($fileId) { 141 | foreach ($this->services as $model) { 142 | $icon = $model->getFileIcon($fileId); 143 | if ($icon !== false) { 144 | return $icon; 145 | } 146 | } 147 | return false; 148 | } 149 | 150 | public function getChannels($excludeArchived = false) { 151 | $channels = []; 152 | foreach ($this->services as $model) { 153 | $channels = array_merge($channels, $model->getChannels($excludeArchived)); 154 | } 155 | return $channels; 156 | } 157 | 158 | /** @deprecated */ 159 | public function getGroups($excludeArchived = false) { 160 | $groups = []; 161 | foreach ($this->services as $model) { 162 | $groups = array_merge($groups, $model->getGroups($excludeArchived)); 163 | } 164 | return $groups; 165 | } 166 | 167 | /** @deprecated */ 168 | public function getIms($excludeDeleted = false) { 169 | $ims = []; 170 | foreach ($this->services as $model) { 171 | $ims = array_merge($ims, $model->getIms($excludeDeleted)); 172 | } 173 | return $ims; 174 | } 175 | 176 | public function openIm(\AlfredSlack\Models\UserModel $user) { 177 | $teamId = $user->getAuth()->team_id; 178 | $model = $this->services[$teamId]; 179 | return $user->openIm($user); 180 | } 181 | 182 | public function getUsers($excludeDeleted = false) { 183 | $users = []; 184 | foreach ($this->services as $model) { 185 | $users = array_merge($users, $model->getUsers($excludeDeleted)); 186 | } 187 | return $users; 188 | } 189 | 190 | public function getFiles() { 191 | $files = []; 192 | foreach ($this->services as $model) { 193 | $files = array_merge($files, $model->getFiles()); 194 | } 195 | return $files; 196 | } 197 | 198 | public function getFile(\AlfredSlack\Models\FileModel $file) { 199 | $teamId = $file->getAuth()->team_id; 200 | $model = $this->services[$teamId]; 201 | return $model->getFile($file); 202 | } 203 | 204 | public function getStarredItems() { 205 | $stars = []; 206 | foreach ($this->services as $model) { 207 | $stars = array_merge($stars, $model->getStarredItems()); 208 | } 209 | return $stars; 210 | } 211 | 212 | public function search($query) { 213 | $res = []; 214 | foreach ($this->services as $model) { 215 | $search = $model->search($query); 216 | $res = $res + $search; 217 | } 218 | return $res; 219 | } 220 | 221 | /** @deprecated */ 222 | public function getImByUser(\AlfredSlack\Models\UserModel $user) { 223 | $teamId = $user->getAuth()->team_id; 224 | $model = $this->services[$teamId]; 225 | return $model->getImByUser($user); 226 | } 227 | 228 | public function setPresence($isAway = false) { 229 | foreach ($this->services as $model) { 230 | $model->setPresence($isAway); 231 | } 232 | } 233 | 234 | public function setStatus(\AlfredSlack\Models\StatusModel $status) { 235 | foreach ($this->services as $model) { 236 | $model->setStatus($status); 237 | } 238 | } 239 | 240 | public function postMessage(\AlfredSlack\Models\ChatInterface $channel, $message, $asBot = false) { 241 | $teamId = $channel->getAuth()->getTeamId(); 242 | $model = $this->services[$teamId]; 243 | return $model->postMessage($channel, $message, $asBot); 244 | } 245 | 246 | public function getChannelHistory(\AlfredSlack\Models\ChannelModel $channel) { 247 | $teamId = $channel->getAuth()->getTeamId(); 248 | $model = $this->services[$teamId]; 249 | return $model->getChannelHistory($channel); 250 | } 251 | 252 | public function getGroupHistory(\AlfredSlack\Models\GroupModel $group) { 253 | $teamId = $group->getAuth()->getTeamId(); 254 | $model = $this->services[$teamId]; 255 | return $model->getGroupHistory($group); 256 | } 257 | 258 | public function getImHistory(\AlfredSlack\Models\ImModel $im) { 259 | $teamId = $im->getAuth()->getTeamId(); 260 | $model = $this->services[$teamId]; 261 | return $model->getImHistory($im); 262 | } 263 | 264 | public function refreshCache() { 265 | foreach ($this->services as $model) { 266 | $model->refreshCache(); 267 | } 268 | } 269 | 270 | public function markGroupAsRead(\AlfredSlack\Models\GroupModel $group) { 271 | $teamId = $group->getAuth()->team_id; 272 | $model = $this->services[$teamId]; 273 | return $model->markGroupAsRead($group); 274 | } 275 | 276 | public function markImAsRead(\AlfredSlack\Models\ImModel $im) { 277 | $teamId = $im->getAuth()->team_id; 278 | $model = $this->services[$teamId]; 279 | return $model->markImAsRead($im); 280 | } 281 | 282 | public function markAllAsRead() { 283 | foreach ($this->services as $model) { 284 | $model->markAllAsRead(); 285 | } 286 | } 287 | 288 | private function parseClientCredentials($clientCredentials) { 289 | if (empty($clientCredentials)) { 290 | throw new \Exception('Missing client credentials'); 291 | } 292 | 293 | list($clientIdAndCode, $clientSecret) = array_pad(explode(':', $clientCredentials, 2), 2, null); 294 | list($clientId, $code) = array_pad(explode('|', $clientIdAndCode, 2), 2, null); 295 | 296 | if (empty($clientId)) { 297 | throw new \Exception('Missing client ID. Please check the generated unique code.'); 298 | } 299 | 300 | if (empty($code)) { 301 | throw new \Exception('Missing OAUTH unique code. Please check the generated unique code.'); 302 | } 303 | 304 | if (empty($clientSecret)) { 305 | throw new \Exception('Missing client secret. Please check the generated unique code.'); 306 | } 307 | 308 | Utils::debug("client ID: $clientId, code: $code, secret: $clientSecret"); 309 | 310 | return [$clientSecret, $clientId, $code]; 311 | } 312 | 313 | private function getErrorMessage($errorCode) { 314 | $message = $errorCode; 315 | switch ($errorCode) { 316 | case 'code_already_used': 317 | $message = 'The unique code was already used.'; 318 | } 319 | return $message; 320 | } 321 | 322 | } 323 | -------------------------------------------------------------------------------- /src/scripts/helpers/service/SingleTeamSlackService.php: -------------------------------------------------------------------------------- 1 | teamId = $teamId; 21 | $this->initCommander(); 22 | } 23 | 24 | private function initCommander() { 25 | $interactor = new MultiCurlInteractor; 26 | $interactor->setResponseFactory(new SlackResponseFactory); 27 | $token = $this->getToken($this->teamId); 28 | if (!empty($token)) { 29 | $this->commander = new CustomCommander($token, $interactor); 30 | } 31 | } 32 | 33 | public function getProfileIcon($userId) { 34 | $icon = Utils::getWorkflows()->readPath('user.image.' . $userId); 35 | if ($icon === false) { 36 | $users = $this->getUsers(true); 37 | $user = Utils::find($users, ['id' => $userId]); 38 | if (!is_null($user)) { 39 | Utils::getWorkflows()->write(file_get_contents($user->profile->image_24), 'user.image.' . $userId); 40 | $icon = Utils::getWorkflows()->readPath('user.image.' . $userId); 41 | } 42 | } 43 | return $icon; 44 | } 45 | 46 | public function getFileIcon($fileId) { 47 | $icon = Utils::getWorkflows()->readPath('file.image.' . $fileId); 48 | if ($icon === false) { 49 | $files = $this->getFiles(); 50 | $file = Utils::find($files, ['id' => $fileId]); 51 | if (is_null($file)) { 52 | $file = $this->getFile($fileId); 53 | } 54 | if (!is_null($file) && property_exists($file, 'thumb_64') && !empty($file->thumb_64)) { 55 | Utils::getWorkflows()->write(file_get_contents($file->thumb_64), 'file.image.' . $fileId); 56 | $icon = Utils::getWorkflows()->readPath('file.image.' . $fileId); 57 | } 58 | } 59 | return $icon; 60 | } 61 | 62 | private function getAuth() { 63 | $auth = Utils::getWorkflows()->read('auth.' . $this->teamId); 64 | if ($auth === false) { 65 | $auth = $this->commander->execute('auth.test')->getBody(); 66 | Utils::getWorkflows()->write($auth, 'auth.' . $this->teamId); 67 | $auth = Utils::getWorkflows()->read('auth.' . $this->teamId); 68 | } 69 | return $auth; 70 | } 71 | 72 | public function getChannels($excludeArchived = false) { 73 | $channels = Utils::getWorkflows()->read('channels.' . $this->teamId); 74 | if ($channels === false) { 75 | $params['types'] = 'public_channel,private_channel'; 76 | if ($excludeArchived === true) { 77 | $params['exclude_archived'] = '1'; 78 | } 79 | $channels = $this->commander->execute('conversations.list', $params)->getBody()['channels']; 80 | $auth = $this->getAuth(); 81 | foreach ($channels as $index => $channel) { 82 | $channels[$index] = Utils::extend($channel, ['auth' => $auth]); 83 | } 84 | Utils::getWorkflows()->write($channels, 'channels.' . $this->teamId); 85 | $channels = Utils::getWorkflows()->read('channels.' . $this->teamId); 86 | } 87 | return ModelFactory::getModels($channels, '\AlfredSlack\Models\ChannelModel'); 88 | } 89 | 90 | /** @deprecated */ 91 | public function getGroups($excludeArchived = false) { 92 | $groups = Utils::getWorkflows()->read('groups.' . $this->teamId); 93 | if ($groups === false) { 94 | $params = []; 95 | if ($excludeArchived === true) { 96 | $params['exclude_archived'] = '1'; 97 | } 98 | $groups = $this->commander->execute('groups.list', $params)->getBody()['groups']; 99 | $auth = $this->getAuth(); 100 | foreach ($groups as $index => $group) { 101 | $groups[$index] = Utils::extend($group, ['auth' => $auth]); 102 | } 103 | Utils::getWorkflows()->write($groups, 'groups.' . $this->teamId); 104 | $groups = Utils::getWorkflows()->read('groups.' . $this->teamId); 105 | } 106 | return ModelFactory::getModels($groups, '\AlfredSlack\Models\GroupModel'); 107 | } 108 | 109 | /** @deprecated */ 110 | public function getIms($excludeDeleted = false) { 111 | $ims = Utils::getWorkflows()->read('ims.' . $this->teamId); 112 | if ($ims === false) { 113 | $ims = $this->commander->execute('conversations.list')->getBody()['channels']; 114 | $auth = $this->getAuth(); 115 | foreach ($ims as $index => $im) { 116 | $ims[$index] = Utils::extend($im, ['auth' => $auth]); 117 | } 118 | if ($excludeDeleted === true) { 119 | $ims = Utils::filter($ims, ['is_user_deleted' => false]); 120 | } 121 | Utils::getWorkflows()->write($ims, 'ims.' . $this->teamId); 122 | $ims = Utils::getWorkflows()->read('ims.' . $this->teamId); 123 | } 124 | return ModelFactory::getModels($ims, '\AlfredSlack\Models\ImModel'); 125 | } 126 | 127 | public function openIm(\AlfredSlack\Models\UserModel $user) { 128 | $userId = $user->getId(); 129 | if (!isset($userId)) { 130 | throw new Exception('The parameter "userId" is mandatory.'); 131 | } 132 | return Utils::toObject($this->commander->execute('im.open', ['user' => $userId])->getBody()); 133 | } 134 | 135 | public function getUsers($excludeDeleted = false) { 136 | $users = Utils::getWorkflows()->read('users.' . $this->teamId); 137 | if ($users === false) { 138 | $users = $this->commander->execute('users.list')->getBody()['members']; 139 | $auth = $this->getAuth(); 140 | foreach ($users as $index => $user) { 141 | $users[$index] = Utils::extend($user, ['auth' => $auth]); 142 | } 143 | Utils::getWorkflows()->write($users, 'users.' . $this->teamId); 144 | $users = Utils::getWorkflows()->read('users.' . $this->teamId); 145 | } 146 | if ($excludeDeleted === true) { 147 | $users = Utils::filter($users, ['deleted' => false]); 148 | } 149 | return ModelFactory::getModels($users, '\AlfredSlack\Models\UserModel'); 150 | } 151 | 152 | public function getFiles() { 153 | $files = Utils::getWorkflows()->read('files.' . $this->teamId); 154 | if ($files === false) { 155 | $files = $this->commander->execute('files.list')->getBody()['files']; 156 | Utils::getWorkflows()->write($files, 'files.' . $this->teamId); 157 | $files = Utils::getWorkflows()->read('files.' . $this->teamId); 158 | } 159 | return ModelFactory::getModels($files, '\AlfredSlack\Models\FileModel'); 160 | } 161 | 162 | public function getFile(\AlfredSlack\Models\FileModel $file) { 163 | return ModelFactory::getModel($this->commander->execute('files.info', ['file' => $file->getId()])->getBody()['file'], '\AlfredSlack\Models\FileModel'); 164 | } 165 | 166 | public function getStarredItems() { 167 | $stars = Utils::getWorkflows()->read('stars.' . $this->teamId); 168 | if ($stars === false) { 169 | $stars = $this->commander->execute('stars.list')->getBody()['items']; 170 | Utils::getWorkflows()->write($stars, 'stars.' . $this->teamId); 171 | $stars = Utils::getWorkflows()->read('stars.' . $this->teamId); 172 | } 173 | return array_map(function ($star) { 174 | switch ($star->type) { 175 | case 'message': 176 | return ModelFactory::getModel($star->message, '\AlfredSlack\Models\MessageModel'); 177 | case 'file': 178 | case 'file_comment': 179 | return ModelFactory::getModel($star->file, '\AlfredSlack\Models\FileModel'); 180 | case 'channel': 181 | return new \AlfredSlack\Models\ChannelModel(['id' => $star->channel]); 182 | } 183 | }, $stars); 184 | } 185 | 186 | public function search($query) { 187 | $items = $this->commander->execute('search.all', ['query' => $query])->getBody(); 188 | $messages = ModelFactory::getModels($items['messages']['matches'], '\AlfredSlack\Models\MessageModel'); 189 | $files = ModelFactory::getModels($items['files']['matches'], '\AlfredSlack\Models\FileModel'); 190 | return $messages + $files; 191 | } 192 | 193 | /** @deprecated */ 194 | public function getImByUser(\AlfredSlack\Models\UserModel $user) { 195 | $userId = $user->getId(); 196 | // Get the IM id if a user 197 | $ims = $this->getIms(true); 198 | $im = Utils::find($ims, ['user' => $userId]); 199 | if (empty($im)) { 200 | $im = $this->openIm($user); 201 | } 202 | return ModelFactory::getModel($im, '\AlfredSlack\Models\ImModel'); 203 | } 204 | 205 | private function getToken($teamId) { 206 | $token = Utils::getWorkflows()->read('token.' . $teamId); 207 | if ($token === false) { 208 | return Utils::getWorkflows()->getPassword('token.' . $teamId); 209 | } else { 210 | return $token; 211 | } 212 | } 213 | 214 | public function setPresence($isAway = false) { 215 | $this->commander->execute('users.setPresence', ['presence' => $isAway ? 'away' : 'auto'])->getBody(); 216 | } 217 | 218 | public function setStatus(\AlfredSlack\Models\StatusModel $status) { 219 | $this->commander->execute('users.profile.set', ['profile' => json_encode($status) ]) ->getBody(); 220 | } 221 | 222 | public function postMessage(\AlfredSlack\Models\ChatInterface $channel, $message, $asBot = false) { 223 | Utils::debug("channel: {$channel->getId()}, message: $message, asBot: $asBot"); 224 | 225 | $id = $channel->getId(); 226 | // if ($channel instanceof \AlfredSlack\Models\UserModel) { 227 | // $id = $this->getImByUser($channel)->getId(); 228 | // } 229 | 230 | return $this->commander->execute('chat.postMessage', [ 231 | 'channel' => $id, 232 | 'text' => $message, 233 | 'as_user' => !$asBot, 234 | 'parse' => 'full', 235 | 'link_names' => 1, 236 | 'unfurl_links' => true, 237 | 'unfurl_media' => true 238 | ])->getBody(); 239 | } 240 | 241 | public function getChannelHistory(\AlfredSlack\Models\ChannelModel $channel) { 242 | return ModelFactory::getModels($this->commander->execute('conversations.history', ['channel' => $channel->getId()])->getBody()['messages'], '\AlfredSlack\Models\MessageModel'); 243 | } 244 | 245 | public function getGroupHistory(\AlfredSlack\Models\GroupModel $group) { 246 | return ModelFactory::getModels($this->commander->execute('groups.history', ['channel' => $group->getId()])->getBody()['messages'], '\AlfredSlack\Models\MessageModel'); 247 | } 248 | 249 | public function getImHistory(\AlfredSlack\Models\ImModel $im) { 250 | return ModelFactory::getModels($this->commander->execute('im.history', ['channel' => $im->getId()])->getBody()['messages'], '\AlfredSlack\Models\MessageModel'); 251 | } 252 | 253 | public function refreshCache() { 254 | 255 | // Refresh auth 256 | Utils::getWorkflows()->delete('auth.' . $this->teamId); 257 | $teamName = $this->getAuth()->team; 258 | Utils::log("Auth refreshed for team $teamName"); 259 | 260 | // Refresh channels 261 | Utils::getWorkflows()->delete('channels.' . $this->teamId); 262 | $channels = $this->getChannels(); 263 | $channels = null; 264 | Utils::log("Channels refreshed for team $teamName"); 265 | 266 | // Refresh groups 267 | Utils::getWorkflows()->delete('groups.' . $this->teamId); 268 | 269 | // Refresh user icons 270 | $users = $this->getUsers(); 271 | foreach ($users as $user) { 272 | Utils::getWorkflows()->delete('user.image.' . $user->getId()); 273 | } 274 | $users = null; 275 | 276 | // Refresh users 277 | Utils::getWorkflows()->delete('users.' . $this->teamId); 278 | $users = $this->getUsers(); 279 | Utils::log("Users refreshed for team $teamName"); 280 | 281 | foreach ($users as $user) { 282 | $this->getProfileIcon($user->getId()); 283 | } 284 | $users = null; 285 | Utils::log("Profile icons refreshed for team $teamName"); 286 | 287 | // Refresh file icons 288 | $files = $this->getFiles(); 289 | foreach ($files as $file) { 290 | Utils::getWorkflows()->delete('file.image.' . $file->getId()); 291 | $this->getFileIcon($file->getId()); 292 | } 293 | $files = null; 294 | Utils::log("File icons refreshed for team $teamName"); 295 | 296 | // Refresh ims 297 | Utils::getWorkflows()->delete('ims.' . $this->teamId); 298 | } 299 | 300 | public function markGroupAsRead(\AlfredSlack\Models\GroupModel $group) { 301 | $now = time(); 302 | $this->commander->executeAsync('groups.mark', ['channel' => $group->getId(), 'ts' => $now]); 303 | } 304 | 305 | public function markImAsRead(\AlfredSlack\Models\ImModel $im) { 306 | $now = time(); 307 | $this->commander->executeAsync('im.mark', ['channel' => $im->getId(), 'ts' => $now]); 308 | } 309 | 310 | public function markAllAsRead() { 311 | $now = time(); 312 | $requests = []; 313 | 314 | return array_map(function ($e) { 315 | return $e->getBody(); 316 | }, $this->commander->executeAll($requests)); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/scripts/helpers/service/SlackServiceInterface.php: -------------------------------------------------------------------------------- 1 | run($config); 14 | -------------------------------------------------------------------------------- /src/scripts/libs/Bootstrap.php: -------------------------------------------------------------------------------- 1 | invoke($route); 16 | } 17 | } 18 | 19 | private function invoke(Route $route) { 20 | $className = 'AlfredSlack\Controllers\\' . ucfirst($route->getController()) . 'Controller'; 21 | $actionName = $route->getAction() . 'Action'; 22 | $controller = new $className(); 23 | 24 | if (!($controller instanceof \AlfredSlack\Controllers\Controller)) { 25 | throw new \Exception("$className must inherits from AlfredSlack\\Controllers\\Controller"); 26 | } 27 | 28 | Utils::log('ACTION: ' . $className . '::' . $actionName . '()'); 29 | Utils::log('SIMULATE: php -r \'$query="' . str_replace('"', '\"', json_encode($route)) . '";include "scripts/index.php";\';'); 30 | 31 | $interruptAction = ($controller->preDispatch($actionName, $route->getParams()) === false); 32 | if (!$interruptAction) { 33 | $actionResult = $controller->dispatch($actionName, $route->getParams()); 34 | $controller->postDispatch($actionName, $route->getParams(), $actionResult); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/scripts/libs/Emoji.php: -------------------------------------------------------------------------------- 1 | emojisByCode = json_decode(file_get_contents(__DIR__ . '/../config/emojis.json'), true); 13 | } 14 | 15 | public static function getInstance() { 16 | if (is_null(static::$_instance)) { 17 | static::$_instance = new static(); 18 | } 19 | return static::$_instance; 20 | } 21 | 22 | public function fromCode($code) { 23 | return isset($this->emojisByCode[$code]) ? $this->emojisByCode[$code] : $code; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/scripts/libs/Query.php: -------------------------------------------------------------------------------- 1 | query = $query; 13 | $this->input = $input; 14 | $this->modifier = $modifier; 15 | } 16 | 17 | public function getQuery() { 18 | return $this->query; 19 | } 20 | 21 | public function isInput() { 22 | return ($this->input === true); 23 | } 24 | 25 | public function getModifier() { 26 | return $this->query; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/scripts/libs/Route.php: -------------------------------------------------------------------------------- 1 | controller = $controller; 13 | $this->action = $action; 14 | $this->params = $params; 15 | } 16 | 17 | public function getController() { 18 | return $this->controller; 19 | } 20 | 21 | public function getAction() { 22 | return $this->action; 23 | } 24 | 25 | public function getParams() { 26 | return $this->params; 27 | } 28 | 29 | public function jsonSerialize() { 30 | return [ 31 | 'controller' => $this->controller, 32 | 'action' => $this->action, 33 | 'params' => $this->params 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/scripts/libs/Router.php: -------------------------------------------------------------------------------- 1 | routes[] = [ 18 | 'path' => $path, 19 | 'definition' => $definition 20 | ]; 21 | return $this; 22 | } 23 | 24 | private static function getInstance() { 25 | if (is_null(static::$_instance)) { 26 | static::$_instance = new static(); 27 | } 28 | return static::$_instance; 29 | } 30 | 31 | /** 32 | * Test if subject match the pattern, and returns the extracted parameters 33 | * @example urlMatch(':action :param', 'alarm 01:00') 34 | * @example urlMatch('sms :who :message', 'sms mommy see you sunday') 35 | * @return {bool|array} false If the subject does not match the pattern, or the array 36 | * of parameters if the subject matches it. 37 | */ 38 | private static function urlMatch($pattern, $subject) { 39 | $pattern = preg_quote($pattern); 40 | 41 | $regexPattern = '/^' . preg_replace(['/\\\\:([^: ]+)\\? /', '/\\\\:([^: ]+) /', '/\\\\:([^:]+)\\?$/', '/\\\\:([^:]+)$/'], ['([^ ]*) ', '([^ ]+) ', '(.*)', '(.+)'], $pattern) . '/'; 42 | 43 | if (is_null($regexPattern)) { 44 | return false; 45 | } 46 | 47 | $paramNames = null; 48 | if (preg_match_all('/\\\\:([^ ]*)/', $pattern, $paramNames)) { 49 | if (count($paramNames) === 2) { 50 | $paramNames = $paramNames[1]; 51 | } else { 52 | $paramNames = []; 53 | } 54 | } else { 55 | $paramNames = []; 56 | } 57 | 58 | $matches = null; 59 | if (preg_match($regexPattern, $subject, $matches)) { 60 | array_shift($matches); 61 | $params = []; 62 | foreach ($paramNames as $index => $value) { 63 | $params[$value] = $matches[$index]; 64 | } 65 | return $params; 66 | } else { 67 | return false; 68 | } 69 | } 70 | 71 | public static function getRoute(Query $config) { 72 | // Get the route unserializing the query (if possible) 73 | $serializedRoute = json_decode($config->getQuery(), true); 74 | if ($serializedRoute !== null) { 75 | $params = []; 76 | if (isset($serializedRoute['params']) && is_array($serializedRoute['params'])) { 77 | // Unserialize models if there are ones 78 | foreach ($serializedRoute['params'] as $key => $value) { 79 | $params[$key] = ModelFactory::isModel($value) ? ModelFactory::getModel($value) : $value; 80 | } 81 | } 82 | return new Route($serializedRoute['controller'], $serializedRoute['action'], $params); 83 | } // Build route from the query as string 84 | else { 85 | $routes = static::getInstance()->routes; 86 | foreach ($routes as $route) { 87 | $params = static::urlMatch($route['path'], $config->getQuery()); 88 | if ($params !== false) { 89 | return new Route($route['definition']['controller'], $route['definition']['action'], $params); 90 | } 91 | } 92 | } 93 | return false; 94 | } 95 | 96 | public static function define($fn) { 97 | if (!is_callable($fn)) { 98 | throw new Exception('Parameter of Router::define must be a function'); 99 | } 100 | $fn(static::getInstance()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/scripts/libs/SlackRouter.php: -------------------------------------------------------------------------------- 1 | 'cacheLocked', 10 | 'type' => 'input' 11 | ], 12 | [ 13 | 'name' => 'getFiles', 14 | 'type' => 'input' 15 | ], 16 | [ 17 | 'name' => 'getStarredItems', 18 | 'type' => 'input' 19 | ], 20 | [ 21 | 'name' => 'search', 22 | 'type' => 'input' 23 | ], 24 | [ 25 | 'name' => 'listPresences', 26 | 'type' => 'input' 27 | ], 28 | [ 29 | 'name' => 'listConfigs', 30 | 'type' => 'input' 31 | ], 32 | [ 33 | 'name' => 'listTeams', 34 | 'type' => 'input' 35 | ], 36 | [ 37 | 'name' => 'getChannelsWithMessage', 38 | 'type' => 'input' 39 | ], 40 | [ 41 | 'name' => 'getChannelHistory', 42 | 'type' => 'input' 43 | ], 44 | [ 45 | 'name' => 'getChannels', 46 | 'type' => 'input' 47 | ], 48 | [ 49 | 'name' => 'saveClient', 50 | 'type' => 'output' 51 | ], 52 | [ 53 | 'name' => 'removeClient', 54 | 'type' => 'output' 55 | ], 56 | [ 57 | 'name' => 'saveTokenUnsafe', 58 | 'type' => 'output' 59 | ], 60 | [ 61 | 'name' => 'setPresence', 62 | 'type' => 'output' 63 | ], 64 | [ 65 | 'name' => 'saveToken', 66 | 'type' => 'output' 67 | ], 68 | [ 69 | 'name' => 'refreshCache', 70 | 'type' => 'output' 71 | ], 72 | [ 73 | 'name' => 'markAllAsRead', 74 | 'type' => 'output' 75 | ], 76 | [ 77 | 'name' => 'openFile', 78 | 'type' => 'output' 79 | ], 80 | [ 81 | 'name' => 'sendMessage', 82 | 'type' => 'output' 83 | ], 84 | [ 85 | 'name' => 'openChannel', 86 | 'type' => 'output' 87 | ] 88 | ]; 89 | 90 | public static function getAction($input, $query) { 91 | $router = new SlackRouter(); 92 | $type = $input ? 'input' : 'output'; 93 | foreach (static::$routingOrder as $action) { 94 | if ($action['type'] === $type) { 95 | $res = $router->{'check' . ucfirst($action['name'])}($query); 96 | if (!empty($res)) { 97 | return (object)$res; 98 | } 99 | } 100 | } 101 | } 102 | 103 | private function __construct() { 104 | 105 | } 106 | 107 | private function checkGetChannels($channel) { 108 | return [ 109 | 'action' => 'getChannels', 110 | 'params' => [$channel] 111 | ]; 112 | } 113 | 114 | private function checkCacheLocked($channel) { 115 | if (Utils::getWorkflows()->readPath('cache.lock') !== false) { 116 | return [ 117 | 'action' => 'getCacheLockedMessage' 118 | ]; 119 | } 120 | } 121 | 122 | private function checkGetChannelsWithMessage($query) { 123 | $firstSpace = strpos($query, ' '); 124 | if (($firstSpace !== false) && ($firstSpace < strlen($query) - 1)) { 125 | $channel = substr($query, 0, $firstSpace); 126 | $message = substr($query, $firstSpace + 1); 127 | 128 | return [ 129 | 'action' => 'getChannels', 130 | 'params' => [$channel, $message] 131 | ]; 132 | } 133 | } 134 | 135 | private function checkGetChannelHistory($query) { 136 | $firstSpace = strpos($query, ' '); 137 | if (($firstSpace !== false) && ($firstSpace === strlen($query) - 1)) { 138 | $channel = substr($query, 0, $firstSpace); 139 | 140 | return [ 141 | 'action' => 'getChannelHistory', 142 | 'params' => [$channel] 143 | ]; 144 | } 145 | } 146 | 147 | private function checkGetFiles($query) { 148 | $firstSpace = strpos($query, ' '); 149 | if (substr($query, 0, $firstSpace) === '--files') { 150 | $search = substr($query, $firstSpace + 1); 151 | 152 | return [ 153 | 'action' => 'getFiles', 154 | 'params' => [$search] 155 | ]; 156 | } 157 | } 158 | 159 | private function checkGetStarredItems($query) { 160 | $firstSpace = strpos($query, ' '); 161 | if (substr($query, 0, $firstSpace) === '--stars') { 162 | $search = substr($query, $firstSpace + 1); 163 | 164 | return [ 165 | 'action' => 'getStarredItems', 166 | 'params' => [$search] 167 | ]; 168 | } 169 | } 170 | 171 | private function checkSearch($query) { 172 | $firstSpace = strpos($query, ' '); 173 | if (substr($query, 0, $firstSpace) === '--search') { 174 | $search = substr($query, $firstSpace + 1); 175 | 176 | return [ 177 | 'action' => 'search', 178 | 'params' => [$search] 179 | ]; 180 | } 181 | } 182 | 183 | private function checkListConfigs($query) { 184 | $arr = explode(' ', $query); 185 | $configAction = $arr[0]; 186 | 187 | if (strpos($configAction, '--') === 0) { 188 | return [ 189 | 'action' => 'listConfigs', 190 | 'params' => $arr 191 | ]; 192 | } 193 | } 194 | 195 | private function checkListPresences($query) { 196 | $arr = explode(' ', $query); 197 | $configAction = $arr[0]; 198 | 199 | if (strpos($configAction, '--presence') === 0) { 200 | return [ 201 | 'action' => 'listPresences', 202 | 'params' => array_slice($arr, 1) 203 | ]; 204 | } 205 | } 206 | 207 | private function checkOpenChannel($query) { 208 | $channelData = json_decode($query); 209 | 210 | return [ 211 | 'action' => 'openChannel', 212 | 'params' => [$channelData] 213 | ]; 214 | } 215 | 216 | private function checkSendMessage($query) { 217 | $channelData = json_decode($query); 218 | 219 | if (!empty($channelData->message)) { 220 | return [ 221 | 'action' => 'sendMessage', 222 | 'params' => [$channelData] 223 | ]; 224 | } 225 | } 226 | 227 | private function checkSaveClient($query) { 228 | $data = json_decode($query); 229 | 230 | if ($data->type === 'client') { 231 | return [ 232 | 'action' => 'saveClient', 233 | 'params' => [$data->clientCredentials] 234 | ]; 235 | } 236 | } 237 | 238 | private function checkRemoveClient($teamName) { 239 | return [ 240 | 'action' => 'removeClient', 241 | 'params' => [$teamName] 242 | ]; 243 | } 244 | 245 | private function checkSaveToken($query) { 246 | $data = json_decode($query); 247 | 248 | if ($data->type === 'token') { 249 | return [ 250 | 'action' => 'saveToken', 251 | 'params' => [$data->token] 252 | ]; 253 | } 254 | } 255 | 256 | private function checkSaveTokenUnsafe($query) { 257 | $data = json_decode($query); 258 | 259 | if ($data->type === 'token-unsafe') { 260 | return [ 261 | 'action' => 'saveTokenUnsafe', 262 | 'params' => [$data->token] 263 | ]; 264 | } 265 | } 266 | 267 | private function checkSetPresence($query) { 268 | $data = json_decode($query); 269 | 270 | if ($data->type === 'presence') { 271 | return [ 272 | 'action' => 'setPresence', 273 | 'params' => [$data->presence] 274 | ]; 275 | } 276 | } 277 | 278 | private function checkMarkAllAsRead($query) { 279 | $data = json_decode($query); 280 | 281 | if ($data->type === 'mark') { 282 | return [ 283 | 'action' => 'markAllAsRead' 284 | ]; 285 | } 286 | } 287 | 288 | private function checkOpenFile($query) { 289 | $data = json_decode($query); 290 | 291 | if ($data->type === 'file') { 292 | return [ 293 | 'action' => 'openFile', 294 | 'params' => [$data] 295 | ]; 296 | } 297 | } 298 | 299 | private function checkRefreshCache($query) { 300 | $data = json_decode($query); 301 | 302 | if ($data->type === 'refresh') { 303 | return [ 304 | 'action' => 'refreshCache' 305 | ]; 306 | } 307 | } 308 | 309 | } 310 | -------------------------------------------------------------------------------- /src/scripts/libs/Utils.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | if (is_array($element) && ($element[$key] !== $value)) { 19 | return false; 20 | } elseif (is_object($element) && ($element->$key !== $value)) { 21 | return false; 22 | } 23 | } 24 | return true; 25 | } else { 26 | return ($element === $predicate); 27 | } 28 | }; 29 | } 30 | } 31 | 32 | public static function extend($a, $b) { 33 | return (object)array_merge((array)$a, (array)$b); 34 | } 35 | 36 | public static function find($array, $predicate) { 37 | if (empty($array)) { 38 | return; 39 | } 40 | 41 | $fn = static::matches($predicate); 42 | foreach ($array as $value) { 43 | if ($fn($value)) { 44 | return $value; 45 | } 46 | } 47 | return; 48 | } 49 | 50 | public static function pluck($collection, $prop) { 51 | return array_map(function ($v) use ($prop) { 52 | return $v[$prop]; 53 | }, $collection); 54 | } 55 | 56 | public static function filter($array, $predicate) { 57 | return array_values(array_filter($array, static::matches($predicate))); 58 | } 59 | 60 | public static function groupBy($collection = null, $iterator = null) { 61 | $result = []; 62 | $collection = (array)$collection; 63 | foreach ($collection as $k => $v) { 64 | $key = is_callable($iterator) ? $iterator($v, $k) : $v[$iterator]; 65 | if (!array_key_exists($key, $result)) { 66 | $result[$key] = []; 67 | } 68 | $result[$key][] = $v; 69 | } 70 | return $result; 71 | } 72 | 73 | public static function flatten(array $array) { 74 | $return = []; 75 | array_walk_recursive($array, function ($a, $k) use (&$return) { 76 | $return[] = $a; 77 | }); 78 | return $return; 79 | } 80 | 81 | public static function toArray($d) { 82 | if (is_object($d)) { 83 | $d = get_object_vars($d); 84 | } 85 | return is_array($d) ? array_map(__METHOD__, $d) : $d; 86 | } 87 | 88 | public static function toObject($d) { 89 | return is_array($d) ? (object)array_map(__METHOD__, $d) : $d; 90 | } 91 | 92 | public static function snakeCase($content, $separator = '_') { 93 | return strtolower(preg_replace('/([^A-Z])([A-Z])/', "$1$separator$2", $content)); 94 | } 95 | 96 | public static function camelCase($content) { 97 | return preg_replace('/_(.?)/', function ($m) { 98 | return strtoupper($m[1]); 99 | }, $content); 100 | } 101 | 102 | public static function pascalCase($content) { 103 | return preg_replace('/(?:^|_)(.?)/', function ($m) { 104 | return strtoupper($m[1]); 105 | }, $content); 106 | } 107 | 108 | public static function log($str) { 109 | error_log($str); 110 | } 111 | 112 | public static function deburr($str) { 113 | return str_replace("'", '', iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str)); 114 | } 115 | 116 | public static function debug($var) { 117 | ob_start(); 118 | var_dump($var); 119 | $trace = ob_get_contents(); 120 | ob_end_clean(); 121 | error_log($trace); 122 | } 123 | 124 | public static function openApp($appName) { 125 | exec('open -a ' . $appName); 126 | } 127 | 128 | public static function openUrl($url) { 129 | exec('open "' . $url . '"'); 130 | } 131 | 132 | public static function getWorkflows() { 133 | if (static::$_workflows === null) { 134 | static::$_workflows = new Workflows(); 135 | } 136 | return static::$_workflows; 137 | } 138 | 139 | public static function defineTimeZone() { 140 | $tz = exec('tz=`ls -l /etc/localtime` && echo ${tz#*/zoneinfo/}'); 141 | date_default_timezone_set($tz); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/scripts/libs/Workflows.php: -------------------------------------------------------------------------------- 1 | path = exec('pwd'); 32 | $this->home = exec('printf $HOME'); 33 | 34 | if (file_exists('info.plist')): 35 | $this->bundle = $this->get('bundleid', 'info.plist'); 36 | elseif (file_exists('info')): 37 | $this->bundle = $this->get('bundleid', 'info'); 38 | elseif (!is_null($bundleid)): 39 | $this->bundle = $bundleid; 40 | endif; 41 | 42 | if (isset($_SERVER['alfred_workflow_cache'])) { 43 | $this->cache = $_SERVER['alfred_workflow_cache']; 44 | } else { 45 | $this->cache = $this->home . "/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/" . $this->bundle; 46 | } 47 | error_log("Cache folder: $this->cache"); 48 | 49 | if (isset($_SERVER['alfred_workflow_data'])) { 50 | $this->data = $_SERVER['alfred_workflow_data']; 51 | } else { 52 | $this->data = $this->home . "/Library/Application Support/Alfred 2/Workflow Data/" . $this->bundle; 53 | } 54 | error_log("Data folder: $this->data"); 55 | 56 | if (!file_exists($this->cache)): 57 | exec("mkdir '" . $this->cache . "'"); 58 | endif; 59 | 60 | if (!file_exists($this->data)): 61 | exec("mkdir '" . $this->data . "'"); 62 | endif; 63 | 64 | $this->results = array(); 65 | } 66 | 67 | /** 68 | * Description: 69 | * Accepts no parameter and returns the value of the bundle id for the current workflow. 70 | * If no value is available, then false is returned. 71 | * 72 | * @param none 73 | * @return false if not available, bundle id value if available. 74 | */ 75 | public function bundle() { 76 | if (is_null($this->bundle)): 77 | return false; 78 | else: 79 | return $this->bundle; 80 | endif; 81 | } 82 | 83 | /** 84 | * Description: 85 | * Accepts no parameter and returns the value of the path to the cache directory for your 86 | * workflow if it is available. Returns false if the value isn't available. 87 | * 88 | * @param none 89 | * @return false if not available, path to the cache directory for your workflow if available. 90 | */ 91 | public function cache() { 92 | if (is_null($this->bundle)): 93 | return false; 94 | else: 95 | if (is_null($this->cache)): 96 | return false; 97 | else: 98 | return $this->cache; 99 | endif; 100 | endif; 101 | } 102 | 103 | /** 104 | * Description: 105 | * Accepts no parameter and returns the value of the path to the storage directory for your 106 | * workflow if it is available. Returns false if the value isn't available. 107 | * 108 | * @param none 109 | * @return false if not available, path to the storage directory for your workflow if available. 110 | */ 111 | public function data() { 112 | if (is_null($this->bundle)): 113 | return false; 114 | else: 115 | if (is_null($this->data)): 116 | return false; 117 | else: 118 | return $this->data; 119 | endif; 120 | endif; 121 | } 122 | 123 | /** 124 | * Description: 125 | * Accepts no parameter and returns the value of the path to the current directory for your 126 | * workflow if it is available. Returns false if the value isn't available. 127 | * 128 | * @param none 129 | * @return false if not available, path to the current directory for your workflow if available. 130 | */ 131 | public function path() { 132 | if (is_null($this->path)): 133 | return false; 134 | else: 135 | return $this->path; 136 | endif; 137 | } 138 | 139 | /** 140 | * Description: 141 | * Accepts no parameter and returns the value of the home path for the current user 142 | * Returns false if the value isn't available. 143 | * 144 | * @param none 145 | * @return false if not available, home path for the current user if available. 146 | */ 147 | public function home() { 148 | if (is_null($this->home)): 149 | return false; 150 | else: 151 | return $this->home; 152 | endif; 153 | } 154 | 155 | /** 156 | * Description: 157 | * Returns an array of available result items 158 | * 159 | * @param none 160 | * @return array - list of result items 161 | */ 162 | public function results() { 163 | return $this->results; 164 | } 165 | 166 | /** 167 | * Description: 168 | * Convert an associative array into XML format 169 | * 170 | * @param $a - An associative array to convert 171 | * @param $format - format of data being passed (json or array), defaults to array 172 | * @return - XML string representation of the array 173 | */ 174 | public function toxml($a = null, $format = 'array') { 175 | 176 | if ($format == 'json'): 177 | $a = json_decode($a, TRUE); 178 | endif; 179 | 180 | if (is_null($a) && !empty($this->results)): 181 | $a = $this->results; 182 | elseif (is_null($a) && empty($this->results)): 183 | return false; 184 | endif; 185 | 186 | $items = new \SimpleXMLElement(""); // Create new XML element 187 | 188 | foreach ($a as $b): // Lop through each object in the array 189 | $c = $items->addChild('item'); // Add a new 'item' element for each object 190 | $c_keys = array_keys($b); // Grab all the keys for that item 191 | foreach ($c_keys as $key): // For each of those keys 192 | if ($key == 'uid'): 193 | $c->addAttribute('uid', $b[$key]); 194 | elseif ($key == 'arg'): 195 | $c->addAttribute('arg', $b[$key]); 196 | elseif ($key == 'type'): 197 | $c->addAttribute('type', $b[$key]); 198 | elseif ($key == 'valid'): 199 | if ($b[$key] == 'yes' || $b[$key] == 'no'): 200 | $c->addAttribute('valid', $b[$key]); 201 | endif; 202 | elseif ($key == 'autocomplete'): 203 | $c->addAttribute('autocomplete', $b[$key]); 204 | elseif ($key == 'icon'): 205 | if (substr($b[$key], 0, 9) == 'fileicon:'): 206 | $val = substr($b[$key], 9); 207 | $c->$key = $val; 208 | $c->$key->addAttribute('type', 'fileicon'); 209 | elseif (substr($b[$key], 0, 9) == 'filetype:'): 210 | $val = substr($b[$key], 9); 211 | $c->$key = $val; 212 | $c->$key->addAttribute('type', 'filetype'); 213 | else: 214 | $c->$key = $b[$key]; 215 | endif; 216 | else: 217 | $c->$key = $b[$key]; 218 | endif; 219 | endforeach; 220 | endforeach; 221 | 222 | return $items->asXML(); // Return XML string representation of the array 223 | 224 | } 225 | 226 | /** 227 | * Description: 228 | * Remove all items from an associative array that do not have a value 229 | * 230 | * @param $a - Associative array 231 | * @return bool 232 | */ 233 | private function empty_filter($a) { 234 | if ($a == '' || $a == null): // if $a is empty or null 235 | return false; // return false, else, return true 236 | else: 237 | return true; 238 | endif; 239 | } 240 | 241 | /** 242 | * Description: 243 | * Save values to a specified plist. If the first parameter is an associative 244 | * array, then the second parameter becomes the plist file to save to. If the 245 | * first parameter is string, then it is assumed that the first parameter is 246 | * the label, the second parameter is the value, and the third parameter is 247 | * the plist file to save the data to. 248 | * 249 | * @param $a - associative array of values to save 250 | * @param $b - the value of the setting 251 | * @param $c - the plist to save the values into 252 | * @return string - execution output 253 | */ 254 | public function set($a = null, $b = null, $c = null) { 255 | if (is_array($a)): 256 | if (file_exists($b)): 257 | if (file_exists($this->path . '/' . $b)): 258 | $b = $this->path . '/' . $b; 259 | endif; 260 | elseif (file_exists($this->data . "/" . $b)): 261 | $b = $this->data . "/" . $b; 262 | elseif (file_exists($this->cache . "/" . $b)): 263 | $b = $this->cache . "/" . $b; 264 | else: 265 | $b = $this->data . "/" . $b; 266 | endif; 267 | else: 268 | if (file_exists($c)): 269 | if (file_exists($this->path . '/' . $c)): 270 | $c = $this->path . '/' . $c; 271 | endif; 272 | elseif (file_exists($this->data . "/" . $c)): 273 | $c = $this->data . "/" . $c; 274 | elseif (file_exists($this->cache . "/" . $c)): 275 | $c = $this->cache . "/" . $c; 276 | else: 277 | $c = $this->data . "/" . $c; 278 | endif; 279 | endif; 280 | 281 | if (is_array($a)): 282 | foreach ($a as $k => $v): 283 | exec('defaults write "' . $b . '" ' . $k . ' "' . $v . '"'); 284 | endforeach; 285 | else: 286 | exec('defaults write "' . $c . '" ' . $a . ' "' . $b . '"'); 287 | endif; 288 | } 289 | 290 | /** 291 | * Description: 292 | * Read a value from the specified plist 293 | * 294 | * @param $a - the value to read 295 | * @param $b - plist to read the values from 296 | * @return bool false if not found, string if found 297 | */ 298 | public function get($a, $b) { 299 | 300 | if (file_exists($b)): 301 | if (file_exists($this->path . '/' . $b)): 302 | $b = $this->path . '/' . $b; 303 | endif; 304 | elseif (file_exists($this->data . "/" . $b)): 305 | $b = $this->data . "/" . $b; 306 | elseif (file_exists($this->cache . "/" . $b)): 307 | $b = $this->cache . "/" . $b; 308 | else: 309 | return false; 310 | endif; 311 | 312 | exec('defaults read "' . $b . '" ' . $a, $out); // Execute system call to read plist value 313 | 314 | if ($out == ""): 315 | return false; 316 | endif; 317 | 318 | $out = $out[0]; 319 | return $out; // Return item value 320 | } 321 | 322 | /** 323 | * Description: 324 | * Read data from a remote file/url, essentially a shortcut for curl 325 | * 326 | * @param $url - URL to request 327 | * @param $options - Array of curl options 328 | * @return result from curl_exec 329 | */ 330 | public function request($url = null, $options = null) { 331 | if (is_null($url)): 332 | return false; 333 | endif; 334 | 335 | $defaults = array( // Create a list of default curl options 336 | CURLOPT_RETURNTRANSFER => true, // Returns the result as a string 337 | CURLOPT_URL => $url, // Sets the url to request 338 | CURLOPT_FRESH_CONNECT => true 339 | ); 340 | 341 | if ($options): 342 | foreach ($options as $k => $v): 343 | $defaults[$k] = $v; 344 | endforeach; 345 | endif; 346 | 347 | array_filter($defaults, // Filter out empty options from the array 348 | array($this, 'empty_filter')); 349 | 350 | $ch = curl_init(); // Init new curl object 351 | curl_setopt_array($ch, $defaults); // Set curl options 352 | $out = curl_exec($ch); // Request remote data 353 | $err = curl_error($ch); 354 | curl_close($ch); // End curl request 355 | 356 | if ($err): 357 | return $err; 358 | else: 359 | return $out; 360 | endif; 361 | } 362 | 363 | /** 364 | * Description: 365 | * Allows searching the local hard drive using mdfind 366 | * 367 | * @param $query - search string 368 | * @return array - array of search results 369 | */ 370 | public function mdfind($query) { 371 | exec('mdfind "' . $query . '"', $results); 372 | return $results; 373 | } 374 | 375 | public function delete($a) { 376 | if (file_exists($a)): 377 | if (file_exists($this->path . '/' . $a)): 378 | unlink($this->path . '/' . $a); 379 | endif; 380 | elseif (file_exists($this->data . "/" . $a)): 381 | unlink($this->data . "/" . $a); 382 | elseif (file_exists($this->cache . "/" . $a)): 383 | unlink($this->cache . "/" . $a); 384 | endif; 385 | } 386 | 387 | /** 388 | * Description: 389 | * Accepts data and a string file name to store data to local file as cache 390 | * 391 | * @param array - data to save to file 392 | * @param file - filename to write the cache data to 393 | * @return none 394 | */ 395 | public function write($a, $b) { 396 | if (file_exists($b)): 397 | if (file_exists($this->path . '/' . $b)): 398 | $b = $this->path . '/' . $b; 399 | endif; 400 | elseif (file_exists($this->data . "/" . $b)): 401 | $b = $this->data . "/" . $b; 402 | elseif (file_exists($this->cache . "/" . $b)): 403 | $b = $this->cache . "/" . $b; 404 | else: 405 | $b = $this->data . "/" . $b; 406 | endif; 407 | 408 | if (is_array($a)): 409 | $a = json_encode($a); 410 | file_put_contents($b, $a); 411 | return true; 412 | elseif (is_string($a)): 413 | file_put_contents($b, $a); 414 | return true; 415 | else: 416 | return false; 417 | endif; 418 | } 419 | 420 | /** 421 | * Description: 422 | * Returns data from a local cache file 423 | * 424 | * @param file - filename to read the cache data from 425 | * @return false if the file cannot be found, the file data if found. If the file 426 | * format is json encoded, then a json object is returned. 427 | */ 428 | public function read($a) { 429 | if (file_exists($a)): 430 | if (file_exists($this->path . '/' . $a)): 431 | $a = $this->path . '/' . $a; 432 | endif; 433 | elseif (file_exists($this->data . "/" . $a)): 434 | $a = $this->data . "/" . $a; 435 | elseif (file_exists($this->cache . "/" . $a)): 436 | $a = $this->cache . "/" . $a; 437 | else: 438 | return false; 439 | endif; 440 | 441 | $out = file_get_contents($a); 442 | if (!is_null(json_decode($out))): 443 | $out = json_decode($out); 444 | endif; 445 | 446 | return $out; 447 | } 448 | 449 | /** 450 | * Description: 451 | * Returns data from a local cache file 452 | * 453 | * @param file - filename to read the cache data from 454 | * @return false if the file cannot be found, the file data if found. If the file 455 | * format is json encoded, then a json object is returned. 456 | */ 457 | public function readPath($a) { 458 | if (file_exists($a)): 459 | if (file_exists($this->path . '/' . $a)): 460 | $a = $this->path . '/' . $a; 461 | endif; 462 | elseif (file_exists($this->data . "/" . $a)): 463 | $a = $this->data . "/" . $a; 464 | elseif (file_exists($this->cache . "/" . $a)): 465 | $a = $this->cache . "/" . $a; 466 | else: 467 | return false; 468 | endif; 469 | 470 | return $a; 471 | } 472 | 473 | public function filetime($a) { 474 | if (file_exists($a)): 475 | if (file_exists($this->path . '/' . $a)): 476 | return filemtime($this->path . '/' . $a); 477 | endif; 478 | elseif (file_exists($this->data . "/" . $a)): 479 | return filemtime($this->data . '/' . $a); 480 | elseif (file_exists($this->cache . "/" . $a)): 481 | return filemtime($this->cache . '/' . $a); 482 | endif; 483 | 484 | return false; 485 | } 486 | 487 | /** 488 | * Description: 489 | * Helper function that just makes it easier to pass values into a function 490 | * and create an array result to be passed back to Alfred 491 | * 492 | * @param $uid - the uid of the result, should be unique 493 | * @param $arg - the argument that will be passed on 494 | * @param $title - The title of the result item 495 | * @param $sub - The subtitle text for the result item 496 | * @param $icon - the icon to use for the result item 497 | * @param $valid - sets whether the result item can be actioned 498 | * @param $auto - the autocomplete value for the result item 499 | * @return array - array item to be passed back to Alfred 500 | */ 501 | public function result($uid, $arg, $title, $sub, $icon, $valid = 'yes', $auto = null, $type = null) { 502 | $temp = array( 503 | 'uid' => $uid, 504 | 'arg' => $arg, 505 | 'title' => $title, 506 | 'subtitle' => $sub, 507 | 'icon' => $icon, 508 | 'valid' => $valid, 509 | 'autocomplete' => $auto, 510 | 'type' => $type 511 | ); 512 | 513 | if (is_null($type)): 514 | unset($temp['type']); 515 | endif; 516 | 517 | array_push($this->results, $temp); 518 | 519 | return $temp; 520 | } 521 | 522 | /** 523 | * Return a password set previously in the system keychain 524 | * Status may equal 44 if no password is defined for this service and this account 525 | * @param $account - the name of the account the password is for 526 | * @param $service - the name of the service, by default it equals the bundle id 527 | * @param string - the password 528 | */ 529 | public function getPassword($account, $service = null) { 530 | if (is_null($service)): 531 | $service = $this->bundle; 532 | endif; 533 | $password = exec("security 2>&1 find-generic-password -s $service -a $account -w", $output, $status); 534 | 535 | return ($status === 0) ? $password : null; 536 | } 537 | 538 | /** 539 | * Store a password in the system keychain 540 | * Status may equal 45 if a password is already defined for this service and this account 541 | * @param $account - the name of the account the password is for 542 | * @param $password - the password to set 543 | * @param $service - the name of the service, by default it equals the bundle id 544 | */ 545 | public function setPassword($account, $password, $service = null) { 546 | if (is_null($service)): 547 | $service = $this->bundle; 548 | endif; 549 | 550 | exec("security 2>&1 add-generic-password -s \"$service\" -a \"$account\" -w \"$password\"", $output, $status); 551 | 552 | // If the password is already set, we override it 553 | if ($status === 45): 554 | exec("security 2>&1 delete-generic-password -s \"$service\" -a \"$account\""); 555 | exec("security 2>&1 add-generic-password -s \"$service\" -a \"$account\" -w \"$password\"", $output, $status); 556 | endif; 557 | 558 | return ($status === 0); 559 | } 560 | 561 | } 562 | -------------------------------------------------------------------------------- /src/scripts/models/AuthModel.php: -------------------------------------------------------------------------------- 1 | auth = new AuthModel($this->auth); 31 | } 32 | 33 | public function __toString() { 34 | return $this->auth->getTeam() . ' - Channel - ' . ($this->num_members === 0 ? 'No' : $this->num_members) . ' members - ' . ($this->is_member ? 'Already a member' : 'Not a member'); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/scripts/models/ChatInterface.php: -------------------------------------------------------------------------------- 1 | thumb_64; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/scripts/models/GroupModel.php: -------------------------------------------------------------------------------- 1 | auth = new AuthModel($this->auth); 30 | } 31 | 32 | public function __toString() { 33 | $numberOfMembers = count($this->members); 34 | return $this->auth->getTeam() . ' - Group - ' . ($numberOfMembers === 0 ? 'No' : $numberOfMembers) . ' members'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/scripts/models/ImModel.php: -------------------------------------------------------------------------------- 1 | auth = new AuthModel($this->auth); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/scripts/models/MessageModel.php: -------------------------------------------------------------------------------- 1 | text); 23 | } 24 | 25 | private static function replaceEmojis($str) { 26 | return preg_replace_callback('/(:[^\\s:]*:)/', function ($matches) { 27 | return Emoji::getInstance()->fromCode($matches[0]); 28 | }, $str); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/scripts/models/Model.php: -------------------------------------------------------------------------------- 1 | $value) { 12 | if (property_exists($this, $key)) { 13 | $this->$key = $value; 14 | } 15 | } 16 | } 17 | } 18 | 19 | public function __call($name, $arguments) { 20 | $action = substr($name, 0, 3); 21 | switch ($action) { 22 | case 'get': 23 | $property = Utils::snakeCase(substr($name, 3)); 24 | if (property_exists($this, $property)) { 25 | return $this->$property; 26 | } else { 27 | throw new \Exception("Undefined property $property"); 28 | } 29 | break; 30 | case 'set': 31 | $property = Utils::snakeCase(substr($name, 3)); 32 | if (property_exists($this, $property)) { 33 | $this->$property = $arguments[0]; 34 | } else { 35 | throw new \Exception("Undefined property $property"); 36 | } 37 | 38 | break; 39 | default : 40 | return FALSE; 41 | } 42 | } 43 | 44 | public function __get($name) { 45 | $property = Utils::snakeCase($name); 46 | if (property_exists($this, $property) && ($name !== '_type')) { 47 | return $this->$property; 48 | } else { 49 | throw new \Exception("Undefined or inaccessible property $property"); 50 | } 51 | } 52 | 53 | public function jsonSerialize() { 54 | $json = array(); 55 | foreach ($this as $key => $value) { 56 | $json[$key] = $value; 57 | } 58 | return $json; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/scripts/models/ModelFactory.php: -------------------------------------------------------------------------------- 1 | _type; 20 | } 21 | 22 | if (is_subclass_of($type, __NAMESPACE__ . '\\' . 'Model')) { 23 | $modelName = $type; 24 | } else { 25 | $modelName = __NAMESPACE__ . '\\' . ucfirst($type) . 'Model'; 26 | } 27 | 28 | return new $modelName($object); 29 | } 30 | 31 | public static function getModels($objects, $type = null) { 32 | return array_map(function ($object) use ($type) { 33 | return static::getModel($object, $type); 34 | }, $objects); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/scripts/models/StatusModel.php: -------------------------------------------------------------------------------- 1 | auth = new AuthModel($this->auth); 32 | } 33 | 34 | public function __toString() { 35 | return $this->auth->getTeam() . ' - User - ' . $this->profile->real_name; 36 | } 37 | 38 | } 39 | --------------------------------------------------------------------------------