├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── chat.go ├── confirmations.go ├── examples ├── all │ └── main.go ├── chat │ └── main.go ├── conf │ └── main.go ├── inventory │ └── main.go ├── market │ └── main.go ├── tradeoffer │ └── main.go ├── versioncheck │ └── main.go └── webapi │ └── main.go ├── filters.go ├── inventory.go ├── login.go ├── market.go ├── profile.go ├── steamapps.go ├── steamid.go ├── store.go ├── totp.go ├── tradeoffer.go ├── twofactor.go └── webapi.go /.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | steam 3 | test.sh 4 | 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | 4 | go: 5 | - 1.6 6 | 7 | script: go build 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | GNU LESSER GENERAL PUBLIC LICENSE 3 | Version 2.1, February 1999 4 | 5 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 6 | 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | [This is the first released version of the Lesser GPL. It also counts 11 | as the successor of the GNU Library Public License, version 2, hence 12 | the version number 2.1.] 13 | 14 | Preamble 15 | 16 | The licenses for most software are designed to take away your 17 | freedom to share and change it. By contrast, the GNU General Public 18 | Licenses are intended to guarantee your freedom to share and change 19 | free software--to make sure the software is free for all its users. 20 | 21 | This license, the Lesser General Public License, applies to some 22 | specially designated software packages--typically libraries--of the 23 | Free Software Foundation and other authors who decide to use it. You 24 | can use it too, but we suggest you first think carefully about whether 25 | this license or the ordinary General Public License is the better 26 | strategy to use in any particular case, based on the explanations 27 | below. 28 | 29 | When we speak of free software, we are referring to freedom of use, 30 | not price. Our General Public Licenses are designed to make sure that 31 | you have the freedom to distribute copies of free software (and charge 32 | for this service if you wish); that you receive source code or can get 33 | it if you want it; that you can change the software and use pieces of 34 | it in new free programs; and that you are informed that you can do 35 | these things. 36 | 37 | To protect your rights, we need to make restrictions that forbid 38 | distributors to deny you these rights or to ask you to surrender these 39 | rights. These restrictions translate to certain responsibilities for 40 | you if you distribute copies of the library or if you modify it. 41 | 42 | For example, if you distribute copies of the library, whether gratis 43 | or for a fee, you must give the recipients all the rights that we gave 44 | you. You must make sure that they, too, receive or can get the source 45 | code. If you link other code with the library, you must provide 46 | complete object files to the recipients, so that they can relink them 47 | with the library after making changes to the library and recompiling 48 | it. And you must show them these terms so they know their rights. 49 | 50 | We protect your rights with a two-step method: (1) we copyright the 51 | library, and (2) we offer you this license, which gives you legal 52 | permission to copy, distribute and/or modify the library. 53 | 54 | To protect each distributor, we want to make it very clear that 55 | there is no warranty for the free library. Also, if the library is 56 | modified by someone else and passed on, the recipients should know 57 | that what they have is not the original version, so that the original 58 | author's reputation will not be affected by problems that might be 59 | introduced by others. 60 | 61 | Finally, software patents pose a constant threat to the existence of 62 | any free program. We wish to make sure that a company cannot 63 | effectively restrict the users of a free program by obtaining a 64 | restrictive license from a patent holder. Therefore, we insist that 65 | any patent license obtained for a version of the library must be 66 | consistent with the full freedom of use specified in this license. 67 | 68 | Most GNU software, including some libraries, is covered by the 69 | ordinary GNU General Public License. This license, the GNU Lesser 70 | General Public License, applies to certain designated libraries, and 71 | is quite different from the ordinary General Public License. We use 72 | this license for certain libraries in order to permit linking those 73 | libraries into non-free programs. 74 | 75 | When a program is linked with a library, whether statically or using 76 | a shared library, the combination of the two is legally speaking a 77 | combined work, a derivative of the original library. The ordinary 78 | General Public License therefore permits such linking only if the 79 | entire combination fits its criteria of freedom. The Lesser General 80 | Public License permits more lax criteria for linking other code with 81 | the library. 82 | 83 | We call this license the "Lesser" General Public License because it 84 | does Less to protect the user's freedom than the ordinary General 85 | Public License. It also provides other free software developers Less 86 | of an advantage over competing non-free programs. These disadvantages 87 | are the reason we use the ordinary General Public License for many 88 | libraries. However, the Lesser license provides advantages in certain 89 | special circumstances. 90 | 91 | For example, on rare occasions, there may be a special need to 92 | encourage the widest possible use of a certain library, so that it 93 | becomes a de-facto standard. To achieve this, non-free programs must 94 | be allowed to use the library. A more frequent case is that a free 95 | library does the same job as widely used non-free libraries. In this 96 | case, there is little to gain by limiting the free library to free 97 | software only, so we use the Lesser General Public License. 98 | 99 | In other cases, permission to use a particular library in non-free 100 | programs enables a greater number of people to use a large body of 101 | free software. For example, permission to use the GNU C Library in 102 | non-free programs enables many more people to use the whole GNU 103 | operating system, as well as its variant, the GNU/Linux operating 104 | system. 105 | 106 | Although the Lesser General Public License is Less protective of the 107 | users' freedom, it does ensure that the user of a program that is 108 | linked with the Library has the freedom and the wherewithal to run 109 | that program using a modified version of the Library. 110 | 111 | The precise terms and conditions for copying, distribution and 112 | modification follow. Pay close attention to the difference between a 113 | "work based on the library" and a "work that uses the library". The 114 | former contains code derived from the library, whereas the latter must 115 | be combined with the library in order to run. 116 | 117 | GNU LESSER GENERAL PUBLIC LICENSE 118 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 119 | 120 | 0. This License Agreement applies to any software library or other 121 | program which contains a notice placed by the copyright holder or 122 | other authorized party saying it may be distributed under the terms of 123 | this Lesser General Public License (also called "this License"). 124 | Each licensee is addressed as "you". 125 | 126 | A "library" means a collection of software functions and/or data 127 | prepared so as to be conveniently linked with application programs 128 | (which use some of those functions and data) to form executables. 129 | 130 | The "Library", below, refers to any such software library or work 131 | which has been distributed under these terms. A "work based on the 132 | Library" means either the Library or any derivative work under 133 | copyright law: that is to say, a work containing the Library or a 134 | portion of it, either verbatim or with modifications and/or translated 135 | straightforwardly into another language. (Hereinafter, translation is 136 | included without limitation in the term "modification".) 137 | 138 | "Source code" for a work means the preferred form of the work for 139 | making modifications to it. For a library, complete source code means 140 | all the source code for all modules it contains, plus any associated 141 | interface definition files, plus the scripts used to control 142 | compilation and installation of the library. 143 | 144 | Activities other than copying, distribution and modification are not 145 | covered by this License; they are outside its scope. The act of 146 | running a program using the Library is not restricted, and output from 147 | such a program is covered only if its contents constitute a work based 148 | on the Library (independent of the use of the Library in a tool for 149 | writing it). Whether that is true depends on what the Library does 150 | and what the program that uses the Library does. 151 | 152 | 1. You may copy and distribute verbatim copies of the Library's 153 | complete source code as you receive it, in any medium, provided that 154 | you conspicuously and appropriately publish on each copy an 155 | appropriate copyright notice and disclaimer of warranty; keep intact 156 | all the notices that refer to this License and to the absence of any 157 | warranty; and distribute a copy of this License along with the 158 | Library. 159 | 160 | You may charge a fee for the physical act of transferring a copy, 161 | and you may at your option offer warranty protection in exchange for a 162 | fee. 163 | 164 | 2. You may modify your copy or copies of the Library or any portion 165 | of it, thus forming a work based on the Library, and copy and 166 | distribute such modifications or work under the terms of Section 1 167 | above, provided that you also meet all of these conditions: 168 | 169 | a) The modified work must itself be a software library. 170 | 171 | b) You must cause the files modified to carry prominent notices 172 | stating that you changed the files and the date of any change. 173 | 174 | c) You must cause the whole of the work to be licensed at no 175 | charge to all third parties under the terms of this License. 176 | 177 | d) If a facility in the modified Library refers to a function or a 178 | table of data to be supplied by an application program that uses 179 | the facility, other than as an argument passed when the facility 180 | is invoked, then you must make a good faith effort to ensure that, 181 | in the event an application does not supply such function or 182 | table, the facility still operates, and performs whatever part of 183 | its purpose remains meaningful. 184 | 185 | (For example, a function in a library to compute square roots has 186 | a purpose that is entirely well-defined independent of the 187 | application. Therefore, Subsection 2d requires that any 188 | application-supplied function or table used by this function must 189 | be optional: if the application does not supply it, the square 190 | root function must still compute square roots.) 191 | 192 | These requirements apply to the modified work as a whole. If 193 | identifiable sections of that work are not derived from the Library, 194 | and can be reasonably considered independent and separate works in 195 | themselves, then this License, and its terms, do not apply to those 196 | sections when you distribute them as separate works. But when you 197 | distribute the same sections as part of a whole which is a work based 198 | on the Library, the distribution of the whole must be on the terms of 199 | this License, whose permissions for other licensees extend to the 200 | entire whole, and thus to each and every part regardless of who wrote 201 | it. 202 | 203 | Thus, it is not the intent of this section to claim rights or contest 204 | your rights to work written entirely by you; rather, the intent is to 205 | exercise the right to control the distribution of derivative or 206 | collective works based on the Library. 207 | 208 | In addition, mere aggregation of another work not based on the Library 209 | with the Library (or with a work based on the Library) on a volume of 210 | a storage or distribution medium does not bring the other work under 211 | the scope of this License. 212 | 213 | 3. You may opt to apply the terms of the ordinary GNU General Public 214 | License instead of this License to a given copy of the Library. To do 215 | this, you must alter all the notices that refer to this License, so 216 | that they refer to the ordinary GNU General Public License, version 2, 217 | instead of to this License. (If a newer version than version 2 of the 218 | ordinary GNU General Public License has appeared, then you can specify 219 | that version instead if you wish.) Do not make any other change in 220 | these notices. 221 | 222 | Once this change is made in a given copy, it is irreversible for 223 | that copy, so the ordinary GNU General Public License applies to all 224 | subsequent copies and derivative works made from that copy. 225 | 226 | This option is useful when you wish to copy part of the code of 227 | the Library into a program that is not a library. 228 | 229 | 4. You may copy and distribute the Library (or a portion or 230 | derivative of it, under Section 2) in object code or executable form 231 | under the terms of Sections 1 and 2 above provided that you accompany 232 | it with the complete corresponding machine-readable source code, which 233 | must be distributed under the terms of Sections 1 and 2 above on a 234 | medium customarily used for software interchange. 235 | 236 | If distribution of object code is made by offering access to copy 237 | from a designated place, then offering equivalent access to copy the 238 | source code from the same place satisfies the requirement to 239 | distribute the source code, even though third parties are not 240 | compelled to copy the source along with the object code. 241 | 242 | 5. A program that contains no derivative of any portion of the 243 | Library, but is designed to work with the Library by being compiled or 244 | linked with it, is called a "work that uses the Library". Such a 245 | work, in isolation, is not a derivative work of the Library, and 246 | therefore falls outside the scope of this License. 247 | 248 | However, linking a "work that uses the Library" with the Library 249 | creates an executable that is a derivative of the Library (because it 250 | contains portions of the Library), rather than a "work that uses the 251 | library". The executable is therefore covered by this License. 252 | Section 6 states terms for distribution of such executables. 253 | 254 | When a "work that uses the Library" uses material from a header file 255 | that is part of the Library, the object code for the work may be a 256 | derivative work of the Library even though the source code is not. 257 | Whether this is true is especially significant if the work can be 258 | linked without the Library, or if the work is itself a library. The 259 | threshold for this to be true is not precisely defined by law. 260 | 261 | If such an object file uses only numerical parameters, data 262 | structure layouts and accessors, and small macros and small inline 263 | functions (ten lines or less in length), then the use of the object 264 | file is unrestricted, regardless of whether it is legally a derivative 265 | work. (Executables containing this object code plus portions of the 266 | Library will still fall under Section 6.) 267 | 268 | Otherwise, if the work is a derivative of the Library, you may 269 | distribute the object code for the work under the terms of Section 6. 270 | Any executables containing that work also fall under Section 6, 271 | whether or not they are linked directly with the Library itself. 272 | 273 | 6. As an exception to the Sections above, you may also combine or 274 | link a "work that uses the Library" with the Library to produce a 275 | work containing portions of the Library, and distribute that work 276 | under terms of your choice, provided that the terms permit 277 | modification of the work for the customer's own use and reverse 278 | engineering for debugging such modifications. 279 | 280 | You must give prominent notice with each copy of the work that the 281 | Library is used in it and that the Library and its use are covered by 282 | this License. You must supply a copy of this License. If the work 283 | during execution displays copyright notices, you must include the 284 | copyright notice for the Library among them, as well as a reference 285 | directing the user to the copy of this License. Also, you must do one 286 | of these things: 287 | 288 | a) Accompany the work with the complete corresponding 289 | machine-readable source code for the Library including whatever 290 | changes were used in the work (which must be distributed under 291 | Sections 1 and 2 above); and, if the work is an executable linked 292 | with the Library, with the complete machine-readable "work that 293 | uses the Library", as object code and/or source code, so that the 294 | user can modify the Library and then relink to produce a modified 295 | executable containing the modified Library. (It is understood 296 | that the user who changes the contents of definitions files in the 297 | Library will not necessarily be able to recompile the application 298 | to use the modified definitions.) 299 | 300 | b) Use a suitable shared library mechanism for linking with the 301 | Library. A suitable mechanism is one that (1) uses at run time a 302 | copy of the library already present on the user's computer system, 303 | rather than copying library functions into the executable, and (2) 304 | will operate properly with a modified version of the library, if 305 | the user installs one, as long as the modified version is 306 | interface-compatible with the version that the work was made with. 307 | 308 | c) Accompany the work with a written offer, valid for at least 309 | three years, to give the same user the materials specified in 310 | Subsection 6a, above, for a charge no more than the cost of 311 | performing this distribution. 312 | 313 | d) If distribution of the work is made by offering access to copy 314 | from a designated place, offer equivalent access to copy the above 315 | specified materials from the same place. 316 | 317 | e) Verify that the user has already received a copy of these 318 | materials or that you have already sent this user a copy. 319 | 320 | For an executable, the required form of the "work that uses the 321 | Library" must include any data and utility programs needed for 322 | reproducing the executable from it. However, as a special exception, 323 | the materials to be distributed need not include anything that is 324 | normally distributed (in either source or binary form) with the major 325 | components (compiler, kernel, and so on) of the operating system on 326 | which the executable runs, unless that component itself accompanies 327 | the executable. 328 | 329 | It may happen that this requirement contradicts the license 330 | restrictions of other proprietary libraries that do not normally 331 | accompany the operating system. Such a contradiction means you cannot 332 | use both them and the Library together in an executable that you 333 | distribute. 334 | 335 | 7. You may place library facilities that are a work based on the 336 | Library side-by-side in a single library together with other library 337 | facilities not covered by this License, and distribute such a combined 338 | library, provided that the separate distribution of the work based on 339 | the Library and of the other library facilities is otherwise 340 | permitted, and provided that you do these two things: 341 | 342 | a) Accompany the combined library with a copy of the same work 343 | based on the Library, uncombined with any other library 344 | facilities. This must be distributed under the terms of the 345 | Sections above. 346 | 347 | b) Give prominent notice with the combined library of the fact 348 | that part of it is a work based on the Library, and explaining 349 | where to find the accompanying uncombined form of the same work. 350 | 351 | 8. You may not copy, modify, sublicense, link with, or distribute 352 | the Library except as expressly provided under this License. Any 353 | attempt otherwise to copy, modify, sublicense, link with, or 354 | distribute the Library is void, and will automatically terminate your 355 | rights under this License. However, parties who have received copies, 356 | or rights, from you under this License will not have their licenses 357 | terminated so long as such parties remain in full compliance. 358 | 359 | 9. You are not required to accept this License, since you have not 360 | signed it. However, nothing else grants you permission to modify or 361 | distribute the Library or its derivative works. These actions are 362 | prohibited by law if you do not accept this License. Therefore, by 363 | modifying or distributing the Library (or any work based on the 364 | Library), you indicate your acceptance of this License to do so, and 365 | all its terms and conditions for copying, distributing or modifying 366 | the Library or works based on it. 367 | 368 | 10. Each time you redistribute the Library (or any work based on the 369 | Library), the recipient automatically receives a license from the 370 | original licensor to copy, distribute, link with or modify the Library 371 | subject to these terms and conditions. You may not impose any further 372 | restrictions on the recipients' exercise of the rights granted herein. 373 | You are not responsible for enforcing compliance by third parties with 374 | this License. 375 | 376 | 11. If, as a consequence of a court judgment or allegation of patent 377 | infringement or for any other reason (not limited to patent issues), 378 | conditions are imposed on you (whether by court order, agreement or 379 | otherwise) that contradict the conditions of this License, they do not 380 | excuse you from the conditions of this License. If you cannot 381 | distribute so as to satisfy simultaneously your obligations under this 382 | License and any other pertinent obligations, then as a consequence you 383 | may not distribute the Library at all. For example, if a patent 384 | license would not permit royalty-free redistribution of the Library by 385 | all those who receive copies directly or indirectly through you, then 386 | the only way you could satisfy both it and this License would be to 387 | refrain entirely from distribution of the Library. 388 | 389 | If any portion of this section is held invalid or unenforceable under 390 | any particular circumstance, the balance of the section is intended to 391 | apply, and the section as a whole is intended to apply in other 392 | circumstances. 393 | 394 | It is not the purpose of this section to induce you to infringe any 395 | patents or other property right claims or to contest validity of any 396 | such claims; this section has the sole purpose of protecting the 397 | integrity of the free software distribution system which is 398 | implemented by public license practices. Many people have made 399 | generous contributions to the wide range of software distributed 400 | through that system in reliance on consistent application of that 401 | system; it is up to the author/donor to decide if he or she is willing 402 | to distribute software through any other system and a licensee cannot 403 | impose that choice. 404 | 405 | This section is intended to make thoroughly clear what is believed to 406 | be a consequence of the rest of this License. 407 | 408 | 12. If the distribution and/or use of the Library is restricted in 409 | certain countries either by patents or by copyrighted interfaces, the 410 | original copyright holder who places the Library under this License 411 | may add an explicit geographical distribution limitation excluding those 412 | countries, so that distribution is permitted only in or among 413 | countries not thus excluded. In such case, this License incorporates 414 | the limitation as if written in the body of this License. 415 | 416 | 13. The Free Software Foundation may publish revised and/or new 417 | versions of the Lesser General Public License from time to time. 418 | Such new versions will be similar in spirit to the present version, 419 | but may differ in detail to address new problems or concerns. 420 | 421 | Each version is given a distinguishing version number. If the Library 422 | specifies a version number of this License which applies to it and 423 | "any later version", you have the option of following the terms and 424 | conditions either of that version or of any later version published by 425 | the Free Software Foundation. If the Library does not specify a 426 | license version number, you may choose any version ever published by 427 | the Free Software Foundation. 428 | 429 | 14. If you wish to incorporate parts of the Library into other free 430 | programs whose distribution conditions are incompatible with these, 431 | write to the author to ask for permission. For software which is 432 | copyrighted by the Free Software Foundation, write to the Free 433 | Software Foundation; we sometimes make exceptions for this. Our 434 | decision will be guided by the two goals of preserving the free status 435 | of all derivatives of our free software and of promoting the sharing 436 | and reuse of software generally. 437 | 438 | NO WARRANTY 439 | 440 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 441 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 442 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 443 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 444 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 445 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 446 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 447 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 448 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 449 | 450 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 451 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 452 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 453 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 454 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 455 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 456 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 457 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 458 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 459 | DAMAGES. 460 | 461 | END OF TERMS AND CONDITIONS 462 | 463 | How to Apply These Terms to Your New Libraries 464 | 465 | If you develop a new library, and you want it to be of the greatest 466 | possible use to the public, we recommend making it free software that 467 | everyone can redistribute and change. You can do so by permitting 468 | redistribution under these terms (or, alternatively, under the terms 469 | of the ordinary General Public License). 470 | 471 | To apply these terms, attach the following notices to the library. 472 | It is safest to attach them to the start of each source file to most 473 | effectively convey the exclusion of warranty; and each file should 474 | have at least the "copyright" line and a pointer to where the full 475 | notice is found. 476 | 477 | 478 | 479 | Copyright (C) 480 | 481 | This library is free software; you can redistribute it and/or 482 | modify it under the terms of the GNU Lesser General Public 483 | License as published by the Free Software Foundation; either 484 | version 2.1 of the License, or (at your option) any later version. 485 | 486 | This library is distributed in the hope that it will be useful, 487 | but WITHOUT ANY WARRANTY; without even the implied warranty of 488 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 489 | Lesser General Public License for more details. 490 | 491 | You should have received a copy of the GNU Lesser General Public 492 | License along with this library; if not, write to the Free Software 493 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 494 | 495 | Also add information on how to contact you by electronic and paper mail. 496 | 497 | You should also get your employer (if you work as a programmer) or 498 | your school, if any, to sign a "copyright disclaimer" for the library, 499 | if necessary. Here is a sample; alter the names: 500 | 501 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 502 | library `Frob' (a library for tweaking knobs) written by James 503 | Random Hacker. 504 | 505 | , 1 April 1990 506 | Ty Coon, President of Vice 507 | 508 | That's all there is to it! 509 | 510 | 511 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Steam [![Build Status](https://travis-ci.org/doctype/steam.svg?branch=master)](https://travis-ci.org/doctype/steam) 2 | 3 | Steam is a library for interactions with [Steam](https://steamcommunity.com), it's written in Go. 4 | Steam tries to keep-it-simple and does not add extra non-sense. There are absolutely no internal-polling or such, 5 | everything is up to you, all it does is wrap around Steam API. 6 | 7 | ## Why? 8 | 9 | - You don't want a library to be "re-trying" automatically 10 | - You don't want a library to be doing your homework 11 | - You are an on-point person and just want stuff that works as-needed 12 | 13 | ## Installation 14 | 15 | Make sure you have _at least_ Go 1.6 with a GOPATH set then run: 16 | 17 | ``` 18 | go get github.com/PuerkitoBio/goquery 19 | go get github.com/doctype/steam 20 | ``` 21 | 22 | ## Example 23 | 24 | ```go 25 | package main 26 | 27 | import ( 28 | "log" 29 | "os" 30 | 31 | "github.com/doctype/steam" 32 | ) 33 | 34 | func main() { 35 | log.SetFlags(log.LstdFlags | log.Lshortfile) 36 | 37 | timeTip, err := steam.GetTimeTip() 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | log.Printf("Time tip: %#v\n", timeTip) 42 | timeDiff := time.Duration(timeTip.Time - time.Now().Unix()) 43 | 44 | session := steam.NewSession(&http.Client{}, "") 45 | if err := session.Login(os.Getenv("steamAccount"), os.Getenv("steamPassword"), os.Getenv("steamSharedSecret"), timeDiff); err != nil { 46 | log.Fatal(err) 47 | } 48 | log.Print("Login successful") 49 | } 50 | ``` 51 | 52 | Find more examples in the examples/ directory. Even better is to read through the source code, it's simple and 53 | straight-forward to understand. 54 | 55 | ## Authors 56 | 57 | - [Ahmed Samy](https://github.com/asamy) 58 | - [Mark Samman](https://github.com/marksamman) 59 | - [Artemiy Ryabinkov](https://github.com/Furdarius) 60 | 61 | ## License 62 | 63 | LGPL 2.1 64 | -------------------------------------------------------------------------------- /chat.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strconv" 10 | ) 11 | 12 | const ( 13 | PersonaStateOffline = iota 14 | PersonaStateOnline 15 | PersonaStateBusy 16 | PersonaStateAway 17 | PersonaStateSnooze 18 | PersonaStateLookingToTrade 19 | PersonaStateLookingToPlay 20 | ) 21 | 22 | const ( 23 | PersonaStateFlagRichPresence = 1 << 0 24 | PersonaStateFlagInJoinableGame = 1 << 1 25 | PersonaStateFlagWeb = 1 << 8 26 | PersonaStateFlagMobile = 1 << 9 27 | PersonaStateFlagBigPicture = 1 << 10 28 | ) 29 | 30 | const ( 31 | MessageTypeStatus = "personastate" 32 | MessageTypeTyping = "typing" 33 | MessageTypeSayText = "saytext" 34 | ) 35 | 36 | const ( 37 | ChatUIModeMobile = "mobile" // empty string works too 38 | ChatUIModeWeb = "web" 39 | ) 40 | 41 | const ( 42 | apiUserPresenceLogin = "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Logon/v1" 43 | apiUserPresenceLogoff = "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Logoff/v1" 44 | apiUserPresencePoll = "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Poll/v1" 45 | apiUserPresenceMessage = "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Message/v1" 46 | ) 47 | 48 | type ChatMessage struct { 49 | Type string `json:"type"` 50 | Text string `json:"text"` 51 | TimestampOff int64 `json:"timestamp"` 52 | UTCTimestamp int64 `json:"utc_timestamp"` 53 | Partner uint32 `json:"accountid_from"` 54 | StatusFlags uint32 `json:"status_flags"` 55 | PersonaState uint32 `json:"persona_state"` 56 | PersonaName string `json:"persona_name"` 57 | } 58 | 59 | type ChatLogMessage struct { 60 | Partner uint32 `json:"m_unAccountID"` 61 | Timestamp int64 `json:"m_tsTimestamp"` 62 | Message string `json:"m_strMessage"` 63 | } 64 | 65 | type ChatResponse struct { 66 | Message int `json:"message"` // Login / Internal 67 | UmqID string `json:"umqid"` // Login / Internal 68 | TimestampOff int64 `json:"timestamp"` // Login 69 | UTCTimestamp int64 `json:"utc_timestamp"` // Login 70 | Push int `json:"push"` // Login 71 | ErrorMessage string `json:"error"` // All (returned as error if not "OK") 72 | MessageBase uint32 `json:"messagebase"` // ChatPoll 73 | LastMessages uint32 `json:"messagelast"` // ChatPoll 74 | Messages []*ChatMessage `json:"messages"` // ChatPoll 75 | SecTimeout uint32 `json:"sectimeout"` // ChatPoll 76 | } 77 | 78 | type ChatFriendResponse struct { 79 | AccountID uint32 `json:"m_unAccountID"` 80 | SteamID SteamID `json:"m_ulSteamID,string"` 81 | Name string `json:"m_strName"` 82 | State uint8 `json:"m_ePersonaState"` 83 | StateFlags uint32 `json:"m_nPersonaStateFlags"` 84 | AvatarHash string `json:"m_strAvatarHash"` 85 | InGame bool `json:"m_bIngame"` 86 | InGameAppID uint64 `json:"m_nInGameAppID,string"` 87 | InGameName string `json:"m_strInGameName"` 88 | LastMessage int64 `json:"m_tsLastMessage"` 89 | LastView int64 `json:"m_tsLastView"` 90 | } 91 | 92 | func (session *Session) ChatLogin(uiMode string) error { 93 | resp, err := session.client.PostForm(apiUserPresenceLogin, url.Values{ 94 | "ui_mode": {uiMode}, 95 | "access_token": {session.oauth.Token}, 96 | }) 97 | if resp != nil { 98 | defer resp.Body.Close() 99 | } 100 | 101 | if err != nil { 102 | return err 103 | } 104 | 105 | if resp.StatusCode != http.StatusOK { 106 | return fmt.Errorf("http error: %d", resp.StatusCode) 107 | } 108 | 109 | var response ChatResponse 110 | if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { 111 | return err 112 | } 113 | 114 | if response.ErrorMessage != "OK" { 115 | return errors.New(response.ErrorMessage) 116 | } 117 | 118 | session.umqID = response.UmqID 119 | session.chatMessage = response.Message 120 | return nil 121 | } 122 | 123 | func (session *Session) ChatLogoff() error { 124 | resp, err := session.client.PostForm(apiUserPresenceLogoff, url.Values{ 125 | "access_token": {session.oauth.Token}, 126 | "umqid": {session.umqID}, 127 | }) 128 | if resp != nil { 129 | resp.Body.Close() 130 | } 131 | 132 | if err != nil { 133 | return err 134 | } 135 | 136 | if resp.StatusCode != http.StatusOK { 137 | return fmt.Errorf("http error: %d", resp.StatusCode) 138 | } 139 | 140 | return nil 141 | } 142 | 143 | func (session *Session) ChatSendMessage(sid SteamID, message, messageType string) error { 144 | resp, err := session.client.PostForm(apiUserPresenceMessage, url.Values{ 145 | "access_token": {session.oauth.Token}, 146 | "steamid_dst": {sid.ToString()}, 147 | "text": {message}, 148 | "type": {messageType}, 149 | "umqid": {session.umqID}, 150 | }) 151 | if resp != nil { 152 | defer resp.Body.Close() 153 | } 154 | 155 | if err != nil { 156 | return err 157 | } 158 | 159 | var response ChatResponse 160 | if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { 161 | return err 162 | } 163 | 164 | if response.ErrorMessage != "OK" { 165 | return errors.New(response.ErrorMessage) 166 | } 167 | 168 | return nil 169 | } 170 | 171 | func (session *Session) ChatPoll(timeoutSeconds string) (*ChatResponse, error) { 172 | resp, err := session.client.PostForm(apiUserPresencePoll, url.Values{ 173 | "umqid": {session.umqID}, 174 | "access_token": {session.oauth.Token}, 175 | "message": {strconv.FormatUint(uint64(session.chatMessage), 10)}, 176 | "pollid": {"1"}, 177 | "sectimeout": {timeoutSeconds}, 178 | "secidletime": {"0"}, 179 | "use_accountids": {"1"}, 180 | }) 181 | if resp != nil { 182 | defer resp.Body.Close() 183 | } 184 | 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | if resp.StatusCode != http.StatusOK { 190 | return nil, fmt.Errorf("http error: %d", resp.StatusCode) 191 | } 192 | 193 | response := &ChatResponse{} 194 | if err := json.NewDecoder(resp.Body).Decode(response); err != nil { 195 | return nil, err 196 | } 197 | 198 | return response, nil 199 | } 200 | 201 | func (session *Session) ChatFriendState(sid SteamID) (*ChatFriendResponse, error) { 202 | resp, err := session.client.Get("https://steamcommunity.com/chat/friendstate/" + strconv.FormatUint(uint64(sid.GetAccountID()), 10)) 203 | if resp != nil { 204 | defer resp.Body.Close() 205 | } 206 | 207 | if err != nil { 208 | return nil, err 209 | } 210 | 211 | if resp.StatusCode != http.StatusOK { 212 | return nil, fmt.Errorf("http error: %d", resp.StatusCode) 213 | } 214 | 215 | response := &ChatFriendResponse{} 216 | if err := json.NewDecoder(resp.Body).Decode(response); err != nil { 217 | return nil, err 218 | } 219 | 220 | return response, nil 221 | } 222 | 223 | func (session *Session) ChatLog(partner uint32) ([]*ChatLogMessage, error) { 224 | resp, err := session.client.PostForm(fmt.Sprintf("https://steamcommunity.com/chat/chatlog/%d", partner), url.Values{ 225 | "sessionid": {session.sessionID}, 226 | }) 227 | if resp != nil { 228 | defer resp.Body.Close() 229 | } 230 | 231 | if err != nil { 232 | return nil, err 233 | } 234 | 235 | log := []*ChatLogMessage{} 236 | if err = json.NewDecoder(resp.Body).Decode(&log); err != nil { 237 | return nil, err 238 | } 239 | 240 | return log, nil 241 | } 242 | -------------------------------------------------------------------------------- /confirmations.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "net/http" 8 | 9 | "fmt" 10 | "net/url" 11 | "strconv" 12 | 13 | "github.com/PuerkitoBio/goquery" 14 | ) 15 | 16 | type Confirmation struct { 17 | ID uint64 18 | Key uint64 19 | Title string 20 | Receiving string 21 | Since string 22 | OfferID uint64 23 | } 24 | 25 | var ( 26 | ErrConfirmationsUnknownError = errors.New("unknown error occurered finding confirmations") 27 | ErrCannotFindConfirmations = errors.New("unable to find confirmations") 28 | ErrCannotFindDescriptions = errors.New("unable to find confirmation descriptions") 29 | ErrConfiramtionsDescMismatch = errors.New("cannot match confirmations with their respective descriptions") 30 | ) 31 | 32 | func (session *Session) execConfirmationRequest(request, key, tag string, current int64, values map[string]interface{}) (*http.Response, error) { 33 | params := url.Values{ 34 | "p": {session.deviceID}, 35 | "a": {session.oauth.SteamID.ToString()}, 36 | "k": {key}, 37 | "t": {strconv.FormatInt(current, 10)}, 38 | "m": {"android"}, 39 | "tag": {tag}, 40 | } 41 | 42 | if values != nil { 43 | for k, v := range values { 44 | switch v := v.(type) { 45 | case string: 46 | params.Add(k, v) 47 | case uint64: 48 | params.Add(k, strconv.FormatUint(v, 10)) 49 | default: 50 | return nil, fmt.Errorf("execConfirmationRequest: missing implementation for type %v", v) 51 | } 52 | } 53 | } 54 | 55 | return session.client.Get("https://steamcommunity.com/mobileconf/" + request + params.Encode()) 56 | } 57 | 58 | func (session *Session) GetConfirmations(identitySecret string, current int64) ([]*Confirmation, error) { 59 | key, err := GenerateConfirmationCode(identitySecret, "conf", current) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | resp, err := session.execConfirmationRequest("conf?", key, "conf", current, nil) 65 | if resp != nil { 66 | defer resp.Body.Close() 67 | } 68 | 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | doc, err := goquery.NewDocumentFromReader(io.Reader(resp.Body)) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | /* FIXME: broken 79 | if empty := doc.Find(".mobileconf_empty"); empty != nil { 80 | if done := doc.Find(".mobileconf_done"); done != nil { 81 | return nil, nil 82 | } 83 | 84 | return nil, ErrConfirmationsUnknownError // FIXME 85 | } 86 | */ 87 | 88 | entries := doc.Find(".mobileconf_list_entry") 89 | if entries == nil { 90 | return nil, ErrCannotFindConfirmations 91 | } 92 | 93 | descriptions := doc.Find(".mobileconf_list_entry_description") 94 | if descriptions == nil { 95 | return nil, ErrCannotFindDescriptions 96 | } 97 | 98 | if len(entries.Nodes) != len(descriptions.Nodes) { 99 | return nil, ErrConfiramtionsDescMismatch 100 | } 101 | 102 | confirmations := []*Confirmation{} 103 | for k, sel := range entries.Nodes { 104 | confirmation := &Confirmation{} 105 | for _, attr := range sel.Attr { 106 | if attr.Key == "data-confid" { 107 | confirmation.ID, _ = strconv.ParseUint(attr.Val, 10, 64) 108 | } else if attr.Key == "data-key" { 109 | confirmation.Key, _ = strconv.ParseUint(attr.Val, 10, 64) 110 | } else if attr.Key == "data-creator" { 111 | confirmation.OfferID, _ = strconv.ParseUint(attr.Val, 10, 64) 112 | } 113 | } 114 | 115 | descSel := descriptions.Nodes[k] 116 | depth := 0 117 | for child := descSel.FirstChild; child != nil; child = child.NextSibling { 118 | for n := child.FirstChild; n != nil; n = n.NextSibling { 119 | switch depth { 120 | case 0: 121 | confirmation.Title = n.Data 122 | case 1: 123 | confirmation.Receiving = n.Data 124 | case 2: 125 | confirmation.Since = n.Data 126 | } 127 | depth++ 128 | } 129 | } 130 | 131 | confirmations = append(confirmations, confirmation) 132 | } 133 | 134 | return confirmations, nil 135 | } 136 | 137 | func (session *Session) AnswerConfirmation(confirmation *Confirmation, identitySecret, answer string, current int64) error { 138 | key, err := GenerateConfirmationCode(identitySecret, answer, current) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | op := map[string]interface{}{ 144 | "op": answer, 145 | "cid": uint64(confirmation.ID), 146 | "ck": confirmation.Key, 147 | } 148 | 149 | resp, err := session.execConfirmationRequest("ajaxop?", key, answer, current, op) 150 | if resp != nil { 151 | defer resp.Body.Close() 152 | } 153 | 154 | if err != nil { 155 | return err 156 | } 157 | 158 | type Response struct { 159 | Success bool `json:"success"` 160 | Message string `json:"message"` 161 | } 162 | 163 | var response Response 164 | if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { 165 | return err 166 | } 167 | 168 | if !response.Success { 169 | return errors.New(response.Message) 170 | } 171 | 172 | return nil 173 | } 174 | 175 | func (confirmation *Confirmation) Answer(session *Session, key, answer string, current int64) error { 176 | return session.AnswerConfirmation(confirmation, key, answer, current) 177 | } 178 | -------------------------------------------------------------------------------- /examples/all/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/doctype/steam" 10 | ) 11 | 12 | func main() { 13 | log.SetFlags(log.LstdFlags | log.Lshortfile) 14 | 15 | timeTip, err := steam.GetTimeTip() 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | log.Printf("Time tip: %#v\n", timeTip) 20 | 21 | timeDiff := time.Duration(timeTip.Time - time.Now().Unix()) 22 | session := steam.NewSession(&http.Client{}, "") 23 | if err := session.Login(os.Getenv("steamAccount"), os.Getenv("steamPassword"), os.Getenv("steamSharedSecret"), timeDiff); err != nil { 24 | log.Fatal(err) 25 | } 26 | log.Print("Login successful") 27 | 28 | profileURL, err := session.GetProfileURL() 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | log.Printf("Profile URL: %s\n", profileURL) 33 | 34 | profileSetting := uint8( /*Profile*/ steam.PrivacyStatePublic | /*Inventory*/ steam.PrivacyStatePublic<<2 | /*Gifts*/ steam.PrivacyStatePublic<<4) 35 | if err = session.SetProfilePrivacy(profileURL, steam.CommentSettingSelf, profileSetting); err != nil { 36 | log.Fatal(err) 37 | } 38 | log.Printf("Done editing profile: %d", profileSetting) 39 | 40 | profileInfo := map[string][]string{ 41 | "personaName": {"MasterOfTests"}, 42 | "summary": {"i am just a test, go away"}, 43 | "customURL": {"therealtesterOFDOOM"}, 44 | } 45 | if err = session.SetProfileInfo(profileURL, &profileInfo); err != nil { 46 | log.Fatal(err) 47 | } 48 | log.Print("Done editing profile info") 49 | 50 | myToken, err := session.GetMyTradeToken() 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | log.Printf("Trade offer token: %s\n", myToken) 55 | 56 | key, err := session.GetWebAPIKey() 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | log.Print("Key: ", key) 61 | 62 | summaries, err := session.GetPlayerSummaries("76561198078821986") 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | log.Printf("Profile summaries: %#v\n", summaries[0]) 67 | 68 | sid := steam.SteamID(76561198078821986) 69 | inven, err := session.GetInventory(sid, 730, 2, true) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | 74 | for _, item := range inven { 75 | log.Printf("Item: %s = %d\n", item.Desc.MarketHashName, item.AssetID) 76 | } 77 | 78 | marketPrices, err := session.GetMarketItemPriceHistory(730, "P90 | Asiimov (Factory New)") 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | 83 | for _, v := range marketPrices { 84 | log.Printf("%s -> %.2f (%s of same price)\n", v.Date, v.Price, v.Count) 85 | } 86 | 87 | overview, err := session.GetMarketItemPriceOverview(730, "DE", "3", "P90 | Asiimov (Factory New)") 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | if overview.Success { 93 | log.Println("Price overfiew for P90 Asiimov FN:") 94 | log.Printf("Volume: %s\n", overview.Volume) 95 | log.Printf("Lowest price: %s Median Price: %s", overview.LowestPrice, overview.MedianPrice) 96 | } 97 | 98 | resp, err := session.GetTradeOffers(steam.TradeFilterSentOffers, time.Now()) 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | 103 | var receiptID uint64 104 | for _, offer := range resp.SentOffers { 105 | var sid steam.SteamID 106 | sid.Parse(offer.Partner, steam.AccountInstanceDesktop, steam.AccountTypeIndividual, steam.UniversePublic) 107 | 108 | if receiptID == 0 && len(offer.RecvItems) != 0 && offer.State == steam.TradeStateAccepted { 109 | receiptID = offer.ReceiptID 110 | } 111 | 112 | log.Printf("Offer id: %d, Receipt ID: %d", offer.ID, offer.ReceiptID) 113 | log.Printf("Offer partner SteamID 64: %d", uint64(sid)) 114 | } 115 | 116 | items, err := session.GetTradeReceivedItems(receiptID) 117 | if err != nil { 118 | log.Fatal(err) 119 | } 120 | 121 | for _, item := range items { 122 | log.Printf("New asset id: %d", item.AssetID) 123 | } 124 | 125 | identity := os.Getenv("steamIdentitySecret") 126 | confirmations, err := session.GetConfirmations(identity, time.Now().Add(timeDiff).Unix()) 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | 131 | for i := range confirmations { 132 | c := confirmations[i] 133 | log.Printf("Confirmation ID: %d, Key: %d\n", c.ID, c.Key) 134 | log.Printf("-> Title %s\n", c.Title) 135 | log.Printf("-> Receiving %s\n", c.Receiving) 136 | log.Printf("-> Since %s\n", c.Since) 137 | log.Printf("-> OfferID %d\n", c.OfferID) 138 | 139 | err = session.AnswerConfirmation(c, key, "allow", time.Now().Add(timeDiff).Unix()) 140 | if err != nil { 141 | log.Fatal(err) 142 | } 143 | 144 | log.Printf("Accepted %d\n", c.ID) 145 | } 146 | 147 | log.Println("Bye!") 148 | } 149 | -------------------------------------------------------------------------------- /examples/chat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/doctype/steam" 10 | ) 11 | 12 | func main() { 13 | log.SetFlags(log.LstdFlags | log.Lshortfile) 14 | 15 | timeTip, err := steam.GetTimeTip() 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | log.Printf("Time tip: %#v\n", timeTip) 20 | 21 | timeDiff := time.Duration(timeTip.Time - time.Now().Unix()) 22 | session := steam.NewSession(&http.Client{}, "") 23 | if err := session.Login(os.Getenv("steamAccount"), os.Getenv("steamPassword"), os.Getenv("steamSharedSecret"), timeDiff); err != nil { 24 | log.Fatal(err) 25 | } 26 | log.Print("Login successful") 27 | 28 | key, err := session.GetWebAPIKey() 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | log.Print("Key: ", key) 33 | 34 | if err = session.ChatLogin(""); err != nil { 35 | log.Fatal(err) 36 | } 37 | defer session.ChatLogoff() 38 | 39 | tries := 0 40 | for { 41 | resp, err := session.ChatPoll("10") 42 | if err != nil { 43 | log.Printf("chatpoll failed: %v\n", err) 44 | time.Sleep(time.Second) 45 | continue 46 | } 47 | 48 | for _, msg := range resp.Messages { 49 | sid := steam.SteamID(0) 50 | sid.ParseDefaults(msg.Partner) 51 | 52 | log.Printf("Message from %d type %s\n", sid, msg.Type) 53 | if msg.Type == steam.MessageTypeSayText { 54 | log.Printf("\tText: %s\n", msg.Text) 55 | if err := session.ChatSendMessage(sid, msg.Text, msg.Type); err != nil { 56 | log.Printf("Failed to send identical message: %v\n", err) 57 | } 58 | } 59 | 60 | if friendState, err := session.ChatFriendState(sid); err != nil { 61 | log.Printf("failed to get friend state for %d: %v\n", sid, err) 62 | } else { 63 | log.Printf("%d: friend state: %#v\n", sid, friendState) 64 | } 65 | } 66 | 67 | tries++ 68 | if tries > 10 { 69 | break 70 | } 71 | 72 | time.Sleep(time.Second * 2) 73 | } 74 | 75 | log.Print("Bye") 76 | } 77 | -------------------------------------------------------------------------------- /examples/conf/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/doctype/steam" 10 | ) 11 | 12 | func main() { 13 | log.SetFlags(log.LstdFlags | log.Lshortfile) 14 | 15 | timeTip, err := steam.GetTimeTip() 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | log.Printf("Time tip: %#v\n", timeTip) 20 | 21 | timeDiff := time.Duration(timeTip.Time - time.Now().Unix()) 22 | session := steam.NewSession(&http.Client{}, "") 23 | if err := session.Login(os.Getenv("steamAccount"), os.Getenv("steamPassword"), os.Getenv("steamSharedSecret"), timeDiff); err != nil { 24 | log.Fatal(err) 25 | } 26 | log.Print("Login successful") 27 | 28 | key, err := session.GetWebAPIKey() 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | log.Print("Key: ", key) 33 | 34 | identitySecret := os.Getenv("steamIdentitySecret") 35 | confirmations, err := session.GetConfirmations(identitySecret, time.Now().Add(timeDiff).Unix()) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | for i := range confirmations { 41 | c := confirmations[i] 42 | log.Printf("Confirmation ID: %d, Key: %d\n", c.ID, c.Key) 43 | log.Printf("-> Title %s\n", c.Title) 44 | log.Printf("-> Receiving %s\n", c.Receiving) 45 | log.Printf("-> Since %s\n", c.Since) 46 | log.Printf("-> OfferID %d\n", c.OfferID) 47 | 48 | err = session.AnswerConfirmation(c, identitySecret, "allow", time.Now().Add(timeDiff).Unix()) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | log.Printf("Accepted %d\n", c.ID) 54 | } 55 | 56 | log.Println("Bye!") 57 | } 58 | -------------------------------------------------------------------------------- /examples/inventory/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/doctype/steam" 10 | ) 11 | 12 | func main() { 13 | log.SetFlags(log.LstdFlags | log.Lshortfile) 14 | 15 | timeTip, err := steam.GetTimeTip() 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | log.Printf("Time tip: %#v\n", timeTip) 20 | timeDiff := time.Duration(timeTip.Time - time.Now().Unix()) 21 | 22 | session := steam.NewSession(&http.Client{}, "") 23 | if err := session.Login(os.Getenv("steamAccount"), os.Getenv("steamPassword"), os.Getenv("steamSharedSecret"), timeDiff); err != nil { 24 | log.Fatal(err) 25 | } 26 | log.Print("Login successful") 27 | 28 | sid := steam.SteamID(76561198078821986) 29 | apps, err := session.GetInventoryAppStats(sid) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | for _, v := range apps { 35 | log.Printf("-- AppID total asset count: %d\n", v.AssetCount) 36 | for _, context := range v.Contexts { 37 | log.Printf("-- Items on %d %d (count %d)\n", v.AppID, context.ID, context.AssetCount) 38 | inven, err := session.GetInventory(sid, v.AppID, context.ID, true) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | for _, item := range inven { 44 | log.Printf("Item: %s = %d\n", item.Desc.MarketHashName, item.AssetID) 45 | } 46 | } 47 | } 48 | 49 | log.Println("Bye!") 50 | } 51 | -------------------------------------------------------------------------------- /examples/market/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/doctype/steam" 10 | ) 11 | 12 | func main() { 13 | log.SetFlags(log.LstdFlags | log.Lshortfile) 14 | 15 | timeTip, err := steam.GetTimeTip() 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | log.Printf("Time tip: %#v\n", timeTip) 20 | 21 | timeDiff := time.Duration(timeTip.Time - time.Now().Unix()) 22 | session := steam.NewSession(&http.Client{}, "") 23 | if err := session.Login(os.Getenv("steamAccount"), os.Getenv("steamPassword"), os.Getenv("steamSharedSecret"), timeDiff); err != nil { 24 | log.Fatal(err) 25 | } 26 | log.Print("Login successful") 27 | 28 | marketPrices, err := session.GetMarketItemPriceHistory(730, "P90 | Asiimov (Factory New)") 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | for _, v := range marketPrices { 34 | log.Printf("%s -> %.2f (%s of same price)\n", v.Date, v.Price, v.Count) 35 | } 36 | 37 | overview, err := session.GetMarketItemPriceOverview(730, "DE", "3", "P90 | Asiimov (Factory New)") 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | if overview.Success { 43 | log.Println("Price overfiew for P90 Asiimov FN:") 44 | log.Printf("Volume: %s\n", overview.Volume) 45 | log.Printf("Lowest price: %s Median Price: %s", overview.LowestPrice, overview.MedianPrice) 46 | } 47 | 48 | if resp, err := session.PlaceBuyOrder(730, 0.03, 1, steam.CurrencyUSD, "Chroma 2 Case Key"); err != nil { 49 | log.Fatal(err) 50 | } else if resp.ErrCode != 1 { 51 | log.Printf("unsuccessful buy order placement: %s\n", resp.ErrMsg) 52 | } else { 53 | log.Printf("Placed buy order id: %d cancelling...\n", resp.OrderID) 54 | if err = session.CancelBuyOrder(resp.OrderID); err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | log.Printf("Successfully cancelled buy order %d\n", resp.OrderID) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/tradeoffer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/doctype/steam" 10 | ) 11 | 12 | func processOffer(session *steam.Session, offer *steam.TradeOffer) { 13 | var sid steam.SteamID 14 | sid.ParseDefaults(offer.Partner) 15 | 16 | log.Printf("Offer id: %d, Receipt ID: %d", offer.ID, offer.ReceiptID) 17 | log.Printf("Offer partner SteamID 64: %d", uint64(sid)) 18 | if offer.State == steam.TradeStateAccepted { 19 | items, err := session.GetTradeReceivedItems(offer.ReceiptID) 20 | if err != nil { 21 | log.Printf("error getting items: %v", err) 22 | } else { 23 | for _, item := range items { 24 | log.Printf("Item: %#v", item) 25 | } 26 | } 27 | } 28 | } 29 | 30 | func main() { 31 | log.SetFlags(log.LstdFlags | log.Lshortfile) 32 | 33 | timeTip, err := steam.GetTimeTip() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | log.Printf("Time tip: %#v\n", timeTip) 38 | 39 | timeDiff := time.Duration(timeTip.Time - time.Now().Unix()) 40 | session := steam.NewSession(&http.Client{}, "") 41 | if err := session.Login(os.Getenv("steamAccount"), os.Getenv("steamPassword"), os.Getenv("steamSharedSecret"), timeDiff); err != nil { 42 | log.Fatal(err) 43 | } 44 | log.Print("Login successful") 45 | 46 | key, err := session.GetWebAPIKey() 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | log.Print("Key: ", key) 51 | 52 | resp, err := session.GetTradeOffers( 53 | steam.TradeFilterSentOffers|steam.TradeFilterRecvOffers, 54 | time.Now(), 55 | ) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | for _, offer := range resp.SentOffers { 61 | processOffer(session, offer) 62 | } 63 | for _, offer := range resp.ReceivedOffers { 64 | processOffer(session, offer) 65 | } 66 | 67 | log.Println("Bye!") 68 | } 69 | -------------------------------------------------------------------------------- /examples/versioncheck/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/doctype/steam" 7 | ) 8 | 9 | func main() { 10 | version, err := steam.NewSessionWithAPIKey("").GetRequiredSteamAppVersion(730) 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | log.Print("Current CS:GO version is: ", version) 15 | } 16 | -------------------------------------------------------------------------------- /examples/webapi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/doctype/steam" 10 | ) 11 | 12 | func main() { 13 | log.SetFlags(log.LstdFlags | log.Lshortfile) 14 | timeTip, err := steam.GetTimeTip() 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | log.Printf("Time tip: %#v\n", timeTip) 19 | 20 | timeDiff := time.Duration(timeTip.Time - time.Now().Unix()) 21 | session := steam.NewSession(&http.Client{}, "") 22 | if err := session.Login(os.Getenv("steamAccount"), os.Getenv("steamPassword"), os.Getenv("steamSharedSecret"), timeDiff); err != nil { 23 | log.Fatal(err) 24 | } 25 | log.Print("Login successful") 26 | 27 | err = session.RevokeWebAPIKey() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | log.Print("Revoked API Key") 32 | 33 | key, err := session.RegisterWebAPIKey("test.org") 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | log.Printf("Registered new API Key: %s", key) 38 | 39 | ownedGames, err := session.GetOwnedGames(steam.SteamID(76561198078821986), false, true) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | log.Printf("Games count: %d\n", ownedGames.Count) 45 | for _, game := range ownedGames.Games { 46 | log.Printf("Game: %d 2 weeks play time: %d\n", game.AppID, game.Playtime2Weeks) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /filters.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | // Filter get InventoryItem and return true if item meet its condition 4 | // false otherwise 5 | type Filter func(*InventoryItem) bool 6 | 7 | // IsTradable return Filter for item.Tradable option 8 | func IsTradable(cond bool) Filter { 9 | return func(item *InventoryItem) bool { 10 | return (item.Desc.Tradable != 0) == cond 11 | } 12 | } 13 | 14 | // IsSouvenir filters souvenir items 15 | func IsSouvenir(cond bool) Filter { 16 | return func(item *InventoryItem) bool { 17 | for _, tag := range item.Desc.Tags { 18 | if tag.Category == "Quality" && tag.InternalName == "tournament" { 19 | return cond 20 | } 21 | } 22 | 23 | return !cond 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /inventory.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/url" 9 | "regexp" 10 | "strconv" 11 | ) 12 | 13 | const ( 14 | InventoryEndpoint = "http://steamcommunity.com/inventory/%d/%d/%d?" 15 | ) 16 | 17 | type ItemTag struct { 18 | Category string `json:"category"` 19 | InternalName string `json:"internal_name"` 20 | LocalizedCategoryName string `json:"localized_category_name"` 21 | LocalizedTagName string `json:"localized_tag_name"` 22 | } 23 | 24 | // Due to the JSON being string, etc... we cannot re-use EconItem 25 | // Also, "assetid" is included as "id" not as assetid. 26 | type InventoryItem struct { 27 | AppID uint32 `json:"appid"` 28 | ContextID uint64 `json:"contextid"` 29 | AssetID uint64 `json:"id,string,omitempty"` 30 | ClassID uint64 `json:"classid,string,omitempty"` 31 | InstanceID uint64 `json:"instanceid,string,omitempty"` 32 | Amount uint64 `json:"amount,string"` 33 | Desc *EconItemDesc `json:"-"` /* May be nil */ 34 | } 35 | 36 | type InventoryContext struct { 37 | ID uint64 `json:"id,string"` /* Apparently context id needs at least 64 bits... */ 38 | AssetCount uint32 `json:"asset_count"` 39 | Name string `json:"name"` 40 | } 41 | 42 | type InventoryAppStats struct { 43 | AppID uint64 `json:"appid"` 44 | Name string `json:"name"` 45 | AssetCount uint32 `json:"asset_count"` 46 | Icon string `json:"icon"` 47 | Link string `json:"link"` 48 | InventoryLogo string `json:"inventory_logo"` 49 | TradePermissions string `json:"trade_permissions"` 50 | Contexts map[string]*InventoryContext `json:"rgContexts"` 51 | } 52 | 53 | var inventoryContextRegexp = regexp.MustCompile("var g_rgAppContextData = (.*?);") 54 | 55 | func (session *Session) fetchInventory( 56 | sid SteamID, 57 | appID, contextID, startAssetID uint64, 58 | filters []Filter, 59 | items *[]InventoryItem, 60 | ) (hasMore bool, lastAssetID uint64, err error) { 61 | params := url.Values{ 62 | "l": {session.language}, 63 | } 64 | 65 | if startAssetID != 0 { 66 | params.Set("start_assetid", strconv.FormatUint(startAssetID, 10)) 67 | params.Set("count", "75") 68 | } else { 69 | params.Set("count", "250") 70 | } 71 | 72 | resp, err := session.client.Get(fmt.Sprintf(InventoryEndpoint, sid, appID, contextID) + params.Encode()) 73 | if resp != nil { 74 | defer resp.Body.Close() 75 | } 76 | 77 | if err != nil { 78 | return false, 0, err 79 | } 80 | 81 | type Asset struct { 82 | AppID uint32 `json:"appid"` 83 | ContextID uint64 `json:"contextid,string"` 84 | AssetID uint64 `json:"assetid,string"` 85 | ClassID uint64 `json:"classid,string"` 86 | InstanceID uint64 `json:"instanceid,string"` 87 | Amount uint64 `json:"amount,string"` 88 | } 89 | 90 | type Response struct { 91 | Assets []Asset `json:"assets"` 92 | Descriptions []*EconItemDesc `json:"descriptions"` 93 | Success int `json:"success"` 94 | HasMore int `json:"more_items"` 95 | LastAssetID string `json:"last_assetid"` 96 | TotalInventoryCount int `json:"total_inventory_count"` 97 | ErrorMsg string `json:"error"` 98 | } 99 | 100 | var response Response 101 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 102 | return false, 0, err 103 | } 104 | 105 | if response.Success == 0 { 106 | if len(response.ErrorMsg) != 0 { 107 | return false, 0, errors.New(response.ErrorMsg) 108 | } 109 | 110 | return false, 0, nil // empty inventory 111 | } 112 | 113 | // Fill in descriptions map, where key 114 | // is "_" pattern, and 115 | // value is position on asset description in 116 | // response.Descriptions array 117 | // 118 | // We need it for fast asset's description 119 | // searching in future 120 | descriptions := make(map[string]int) 121 | for i, desc := range response.Descriptions { 122 | key := fmt.Sprintf("%d_%d", desc.ClassID, desc.InstanceID) 123 | descriptions[key] = i 124 | } 125 | 126 | for _, asset := range response.Assets { 127 | var desc *EconItemDesc 128 | 129 | key := fmt.Sprintf("%d_%d", asset.ClassID, asset.InstanceID) 130 | if d, ok := descriptions[key]; ok { 131 | desc = response.Descriptions[d] 132 | } 133 | 134 | item := InventoryItem{ 135 | AppID: asset.AppID, 136 | ContextID: asset.ContextID, 137 | AssetID: asset.AssetID, 138 | ClassID: asset.ClassID, 139 | InstanceID: asset.InstanceID, 140 | Amount: asset.Amount, 141 | Desc: desc, 142 | } 143 | 144 | add := true 145 | for _, filter := range filters { 146 | add = filter(&item) 147 | if !add { 148 | break 149 | } 150 | } 151 | 152 | if add { 153 | *items = append(*items, item) 154 | } 155 | } 156 | 157 | hasMore = response.HasMore != 0 158 | if !hasMore { 159 | return hasMore, 0, nil 160 | } 161 | 162 | lastAssetID, err = strconv.ParseUint(response.LastAssetID, 10, 64) 163 | if err != nil { 164 | return hasMore, 0, err 165 | } 166 | 167 | return hasMore, lastAssetID, nil 168 | } 169 | 170 | func (session *Session) GetInventory(sid SteamID, appID, contextID uint64, tradableOnly bool) ([]InventoryItem, error) { 171 | filters := []Filter{} 172 | 173 | if tradableOnly { 174 | filters = append(filters, IsTradable(tradableOnly)) 175 | } 176 | 177 | return session.GetFilterableInventory(sid, appID, contextID, filters) 178 | } 179 | 180 | func (session *Session) GetFilterableInventory(sid SteamID, appID, contextID uint64, filters []Filter) ([]InventoryItem, error) { 181 | items := []InventoryItem{} 182 | startAssetID := uint64(0) 183 | 184 | for { 185 | hasMore, lastAssetID, err := session.fetchInventory(sid, appID, contextID, startAssetID, filters, &items) 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | if !hasMore { 191 | break 192 | } 193 | 194 | startAssetID = lastAssetID 195 | } 196 | 197 | return items, nil 198 | } 199 | 200 | func (session *Session) GetInventoryAppStats(sid SteamID) (map[string]InventoryAppStats, error) { 201 | resp, err := session.client.Get("https://steamcommunity.com/profiles/" + sid.ToString() + "/inventory") 202 | if resp != nil { 203 | defer resp.Body.Close() 204 | } 205 | 206 | if err != nil { 207 | return nil, err 208 | } 209 | 210 | body, err := ioutil.ReadAll(resp.Body) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | m := inventoryContextRegexp.FindSubmatch(body) 216 | if m == nil || len(m) != 2 { 217 | return nil, err 218 | } 219 | 220 | inven := map[string]InventoryAppStats{} 221 | if err = json.Unmarshal(m[1], &inven); err != nil { 222 | return nil, err 223 | } 224 | 225 | return inven, nil 226 | } 227 | -------------------------------------------------------------------------------- /login.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "encoding/base64" 8 | "encoding/hex" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "math/big" 13 | "net/http" 14 | "net/http/cookiejar" 15 | "net/url" 16 | "strconv" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | type LoginResponse struct { 22 | Success bool `json:"success"` 23 | PublicKeyMod string `json:"publickey_mod"` 24 | PublicKeyExp string `json:"publickey_exp"` 25 | Timestamp string 26 | TokenGID string 27 | } 28 | 29 | type OAuth struct { 30 | SteamID SteamID `json:"steamid,string"` 31 | Token string `json:"oauth_token"` 32 | WGToken string `json:"wgtoken"` 33 | WGTokenSecure string `json:"wgtoken_secure"` 34 | WebCookie string `json:"webcookie"` 35 | } 36 | 37 | type LoginSession struct { 38 | Success bool `json:"success"` 39 | LoginComplete bool `json:"login_complete"` 40 | RequiresTwoFactor bool `json:"requires_twofactor"` 41 | Message string `json:"message"` 42 | RedirectURI string `json:"redirect_uri"` 43 | OAuthInfo string `json:"oauth"` 44 | } 45 | 46 | type Session struct { 47 | client *http.Client 48 | oauth OAuth 49 | sessionID string 50 | apiKey string 51 | deviceID string 52 | umqID string 53 | chatMessage int 54 | language string 55 | } 56 | 57 | const ( 58 | httpXRequestedWithValue = "com.valvesoftware.android.steam.community" 59 | httpUserAgentValue = "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30" 60 | httpAcceptValue = "text/javascript, text/html, application/xml, text/xml, */*" 61 | ) 62 | 63 | var ( 64 | ErrInvalidUsername = errors.New("invalid username") 65 | ErrNeedTwoFactor = errors.New("invalid twofactor code") 66 | ) 67 | 68 | func (session *Session) proceedDirectLogin(response *LoginResponse, accountName, password, twoFactorCode string) error { 69 | var n big.Int 70 | n.SetString(response.PublicKeyMod, 16) 71 | 72 | exp, err := strconv.ParseInt(response.PublicKeyExp, 16, 32) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | pub := rsa.PublicKey{N: &n, E: int(exp)} 78 | rsaOut, err := rsa.EncryptPKCS1v15(rand.Reader, &pub, []byte(password)) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | req, err := http.NewRequest( 84 | http.MethodPost, 85 | "https://steamcommunity.com/login/dologin/?"+url.Values{ 86 | "captcha_text": {""}, 87 | "captchagid": {"-1"}, 88 | "emailauth": {""}, 89 | "emailsteamid": {""}, 90 | "password": {base64.StdEncoding.EncodeToString(rsaOut)}, 91 | "remember_login": {"true"}, 92 | "rsatimestamp": {response.Timestamp}, 93 | "twofactorcode": {twoFactorCode}, 94 | "username": {accountName}, 95 | "oauth_client_id": {"DE45CD61"}, 96 | "oauth_scope": {"read_profile write_profile read_client write_client"}, 97 | "loginfriendlyname": {"#login_emailauth_friendlyname_mobile"}, 98 | "donotcache": {strconv.FormatInt(time.Now().Unix()*1000, 10)}, 99 | }.Encode(), 100 | nil, 101 | ) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | req.Header.Add("X-Requested-With", httpXRequestedWithValue) 107 | req.Header.Add("Referer", "https://steamcommunity.com/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client") 108 | req.Header.Add("User-Agent", httpUserAgentValue) 109 | req.Header.Add("Accept", httpAcceptValue) 110 | 111 | resp, err := session.client.Do(req) 112 | if resp != nil { 113 | defer resp.Body.Close() 114 | } 115 | 116 | if err != nil { 117 | return err 118 | } 119 | 120 | var loginSession LoginSession 121 | if err := json.NewDecoder(resp.Body).Decode(&loginSession); err != nil { 122 | return err 123 | } 124 | 125 | if !loginSession.Success { 126 | if loginSession.RequiresTwoFactor { 127 | return ErrNeedTwoFactor 128 | } 129 | 130 | return errors.New(loginSession.Message) 131 | } 132 | 133 | if err := json.Unmarshal([]byte(loginSession.OAuthInfo), &session.oauth); err != nil { 134 | return err 135 | } 136 | 137 | randomBytes := make([]byte, 6) 138 | if _, err := rand.Read(randomBytes); err != nil { 139 | return err 140 | } 141 | 142 | sessionID := make([]byte, hex.EncodedLen(len(randomBytes))) 143 | hex.Encode(sessionID, randomBytes) 144 | session.sessionID = string(sessionID) 145 | 146 | url, _ := url.Parse("https://steamcommunity.com") 147 | cookies := session.client.Jar.Cookies(url) 148 | for _, cookie := range cookies { 149 | if cookie.Name == "mobileClient" || cookie.Name == "mobileClientVersion" || cookie.Name == "steamCountry" || strings.Contains(cookie.Name, "steamMachineAuth") { 150 | // remove by setting max age -1 151 | cookie.MaxAge = -1 152 | } 153 | } 154 | 155 | sum := md5.Sum([]byte(accountName + password)) 156 | session.deviceID = fmt.Sprintf( 157 | "android:%x-%x-%x-%x-%x", 158 | sum[:2], sum[2:4], sum[4:6], sum[6:8], sum[8:10], 159 | ) 160 | 161 | session.client.Jar.SetCookies( 162 | url, 163 | append(cookies, &http.Cookie{ 164 | Name: "sessionid", 165 | Value: session.sessionID, 166 | }), 167 | ) 168 | return nil 169 | } 170 | 171 | func (session *Session) makeLoginRequest(accountName, password string) (*LoginResponse, error) { 172 | req, err := http.NewRequest(http.MethodPost, "https://steamcommunity.com/login/getrsakey?username="+accountName, nil) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | jar, err := cookiejar.New(nil) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | req.Header.Add("X-Requested-With", httpXRequestedWithValue) 183 | req.Header.Add("Referer", "https://steamcommunity.com/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client") 184 | req.Header.Add("User-Agent", httpUserAgentValue) 185 | req.Header.Add("Accept", httpAcceptValue) 186 | 187 | cookies := []*http.Cookie{ 188 | {Name: "mobileClientVersion", Value: "0 (2.1.3)"}, 189 | {Name: "mobileClient", Value: "android"}, 190 | {Name: "Steam_Language", Value: session.language}, 191 | {Name: "timezoneOffset", Value: "0,0"}, 192 | } 193 | url, _ := url.Parse("https://steamcommunity.com") 194 | jar.SetCookies(url, cookies) 195 | session.client.Jar = jar 196 | 197 | resp, err := session.client.Do(req) 198 | if resp != nil { 199 | defer resp.Body.Close() 200 | } 201 | 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | var response LoginResponse 207 | if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { 208 | return nil, err 209 | } 210 | 211 | if !response.Success { 212 | return nil, ErrInvalidUsername 213 | } 214 | 215 | return &response, nil 216 | } 217 | 218 | // LoginTwoFactorCode logs in with the @twoFactorCode provided, 219 | // note that in the case of having shared secret known, then it's better to 220 | // use Login() because it's more accurate. 221 | // Note: You can provide an empty two factor code if two factor authentication is not 222 | // enabled on the account provided. 223 | func (session *Session) LoginTwoFactorCode(accountName, password, twoFactorCode string) error { 224 | response, err := session.makeLoginRequest(accountName, password) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | return session.proceedDirectLogin(response, accountName, password, twoFactorCode) 230 | } 231 | 232 | // Login requests log in information first, then generates two factor code, and proceeds 233 | // to do the actual login, this provides a better chance that the code generated will work 234 | // because of the slowness of the API. 235 | func (session *Session) Login(accountName, password, sharedSecret string, timeOffset time.Duration) error { 236 | response, err := session.makeLoginRequest(accountName, password) 237 | if err != nil { 238 | return err 239 | } 240 | 241 | var twoFactorCode string 242 | if len(sharedSecret) != 0 { 243 | if twoFactorCode, err = GenerateTwoFactorCode(sharedSecret, time.Now().Add(timeOffset).Unix()); err != nil { 244 | return err 245 | } 246 | } 247 | 248 | return session.proceedDirectLogin(response, accountName, password, twoFactorCode) 249 | } 250 | 251 | func (session *Session) GetSteamID() SteamID { 252 | return session.oauth.SteamID 253 | } 254 | 255 | func (session *Session) SetLanguage(lang string) { 256 | session.language = lang 257 | } 258 | 259 | func NewSessionWithAPIKey(apiKey string) *Session { 260 | return &Session{ 261 | client: &http.Client{}, 262 | apiKey: apiKey, 263 | language: "english", 264 | } 265 | } 266 | 267 | func NewSession(client *http.Client, apiKey string) *Session { 268 | return &Session{ 269 | client: client, 270 | apiKey: apiKey, 271 | language: "english", 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /market.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | const ( 14 | CurrencyUSD = "1" 15 | CurrencyGBP = "2" 16 | CurrencyEUR = "3" 17 | CurrencyCHF = "4" 18 | CurrencyRUB = "5" 19 | CurrencyPLN = "6" 20 | CurrencyBRL = "7" 21 | CurrencyJPY = "8" 22 | CurrencyNOK = "9" 23 | CurrencyIDR = "10" 24 | CurrencyMYR = "11" 25 | CurrencyPHP = "12" 26 | CurrencySGD = "13" 27 | CurrencyTHB = "14" 28 | CurrencyVND = "15" 29 | CurrencyKRW = "16" 30 | CurrencyTRY = "17" 31 | CurrencyUAH = "18" 32 | CurrencyMXN = "19" 33 | CurrencyCAD = "20" 34 | CurrencyAUD = "21" 35 | CurrencyNZD = "22" 36 | CurrencyCNY = "23" 37 | CurrencyINR = "24" 38 | CurrencyCLP = "25" 39 | CurrencyPEN = "26" 40 | CurrencyCOP = "27" 41 | CurrencyZAR = "28" 42 | CurrencyHKD = "29" 43 | CurrencyTWD = "30" 44 | CurrencySAR = "31" 45 | CurrencyAED = "32" 46 | CurrencyARS = "34" 47 | CurrencyILS = "35" 48 | CurrencyBYN = "36" 49 | CurrencyKZT = "37" 50 | CurrencyKWD = "38" 51 | CurrencyQAR = "39" 52 | CurrencyCRC = "40" 53 | CurrencyUYU = "41" 54 | CurrencyRMB = "9000" 55 | ) 56 | 57 | type MarketItemPriceOverview struct { 58 | Success bool `json:"success"` 59 | LowestPrice string `json:"lowest_price"` 60 | MedianPrice string `json:"median_price"` 61 | Volume string `json:"volume"` 62 | } 63 | 64 | type MarketItemPrice struct { 65 | Date string 66 | Price float64 67 | Count string 68 | } 69 | 70 | type MarketItemResponse struct { 71 | Success bool `json:"success"` 72 | PricePrefix string `json:"price_prefix"` 73 | PriceSuffix string `json:"price_suffix"` 74 | Prices interface{} `json:"prices"` 75 | } 76 | 77 | type MarketSellResponse struct { 78 | Success bool `json:"success"` 79 | RequiresConfirmation uint32 `json:"requires_confirmation"` 80 | MobileConfirmationRequired bool `json:"needs_mobile_confirmation"` 81 | EmailConfirmationRequired bool `json:"needs_email_confirmation"` 82 | EmailDomain string `json:"email_domain"` 83 | } 84 | 85 | type MarketBuyOrderResponse struct { 86 | ErrCode int `json:"success"` 87 | ErrMsg string `json:"message"` // Set if ErrCode != 1 88 | OrderID uint64 `json:"buy_orderid,string"` 89 | } 90 | 91 | var ( 92 | ErrCannotLoadPrices = errors.New("unable to load prices at this time") 93 | ErrInvalidPriceResponse = errors.New("invalid market pricehistory response") 94 | ) 95 | 96 | func (session *Session) GetMarketItemPriceHistory(appID uint64, marketHashName string) ([]*MarketItemPrice, error) { 97 | resp, err := session.client.Get("https://steamcommunity.com/market/pricehistory/?" + url.Values{ 98 | "appid": {strconv.FormatUint(appID, 10)}, 99 | "market_hash_name": {marketHashName}, 100 | }.Encode()) 101 | if resp != nil { 102 | defer resp.Body.Close() 103 | } 104 | 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | if resp.StatusCode != http.StatusOK { 110 | return nil, fmt.Errorf("http error: %d", resp.StatusCode) 111 | } 112 | 113 | response := MarketItemResponse{} 114 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 115 | return nil, err 116 | } 117 | 118 | if !response.Success { 119 | return nil, ErrCannotLoadPrices 120 | } 121 | 122 | var prices []interface{} 123 | var ok bool 124 | if prices, ok = response.Prices.([]interface{}); !ok { 125 | return nil, ErrCannotLoadPrices 126 | } 127 | 128 | items := []*MarketItemPrice{} 129 | for _, v := range prices { 130 | if v, ok := v.([]interface{}); ok { 131 | item := &MarketItemPrice{} 132 | for _, val := range v { 133 | switch val := val.(type) { 134 | case string: 135 | if len(item.Date) != 0 { 136 | item.Count = val 137 | } else { 138 | item.Date = val 139 | } 140 | case float64: 141 | item.Price = val 142 | } 143 | } 144 | items = append(items, item) 145 | } 146 | } 147 | return items, nil 148 | } 149 | 150 | func (session *Session) GetMarketItemPriceOverview(appID uint64, country, currencyID, marketHashName string) (*MarketItemPriceOverview, error) { 151 | resp, err := session.client.Get("https://steamcommunity.com/market/priceoverview/?" + url.Values{ 152 | "appid": {strconv.FormatUint(appID, 10)}, 153 | "country": {country}, 154 | "currencyID": {currencyID}, 155 | "market_hash_name": {marketHashName}, 156 | }.Encode()) 157 | if resp != nil { 158 | defer resp.Body.Close() 159 | } 160 | 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | if resp.StatusCode != http.StatusOK { 166 | return nil, fmt.Errorf("http error: %d", resp.StatusCode) 167 | } 168 | 169 | overview := &MarketItemPriceOverview{} 170 | if err = json.NewDecoder(resp.Body).Decode(overview); err != nil { 171 | return nil, err 172 | } 173 | 174 | return overview, nil 175 | } 176 | 177 | func (session *Session) SellItem(item *InventoryItem, amount, price uint64) (*MarketSellResponse, error) { 178 | resp, err := session.client.PostForm("https://steamcommunity.com/market/sellitem/", url.Values{ 179 | "amount": {strconv.FormatUint(amount, 10)}, 180 | "appid": {strconv.FormatUint(uint64(item.AppID), 10)}, 181 | "assetid": {strconv.FormatUint(item.AssetID, 10)}, 182 | "contextid": {strconv.FormatUint(item.ContextID, 10)}, 183 | "price": {strconv.FormatUint(price, 10)}, 184 | "sessionid": {session.sessionID}, 185 | }) 186 | if resp != nil { 187 | defer resp.Body.Close() 188 | } 189 | 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | if resp.StatusCode != http.StatusOK { 195 | return nil, fmt.Errorf("http error: %d", resp.StatusCode) 196 | } 197 | 198 | response := &MarketSellResponse{} 199 | if err = json.NewDecoder(resp.Body).Decode(response); err != nil { 200 | return nil, err 201 | } 202 | 203 | return response, nil 204 | } 205 | 206 | func (session *Session) PlaceBuyOrder(appid uint64, priceTotal float64, quantity uint64, currencyID, marketHashName string) (*MarketBuyOrderResponse, error) { 207 | req, err := http.NewRequest( 208 | http.MethodPost, 209 | "https://steamcommunity.com/market/createbuyorder/", 210 | strings.NewReader(url.Values{ 211 | "appid": {strconv.FormatUint(appid, 10)}, 212 | "currency": {currencyID}, 213 | "market_hash_name": {marketHashName}, 214 | "price_total": {strconv.FormatUint(uint64(priceTotal*100), 10)}, 215 | "quantity": {strconv.FormatUint(quantity, 10)}, 216 | "sessionid": {session.sessionID}, 217 | }.Encode()), 218 | ) 219 | if err != nil { 220 | return nil, err 221 | } 222 | 223 | var referer string 224 | referer = strings.Replace(marketHashName, " ", "%20", -1) 225 | referer = strings.Replace(referer, "#", "%23", -1) 226 | 227 | req.Header.Add( 228 | "Referer", 229 | fmt.Sprintf("https://steamcommunity.com/market/listings/%d/%s", appid, referer ), 230 | ) 231 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 232 | 233 | resp, err := session.client.Do(req) 234 | if resp != nil { 235 | defer resp.Body.Close() 236 | } 237 | 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | response := &MarketBuyOrderResponse{} 243 | if err = json.NewDecoder(resp.Body).Decode(response); err != nil { 244 | return nil, err 245 | } 246 | 247 | return response, nil 248 | } 249 | 250 | func (session *Session) CancelBuyOrder(orderid uint64) error { 251 | req, err := http.NewRequest( 252 | http.MethodPost, 253 | "https://steamcommunity.com/market/cancelbuyorder/", 254 | strings.NewReader(url.Values{ 255 | "sessionid": {session.sessionID}, 256 | "buy_orderid": {strconv.FormatUint(orderid, 10)}, 257 | }.Encode()), 258 | ) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | req.Header.Add("Referer", "https://steamcommunity.com/market") 264 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 265 | 266 | resp, err := session.client.Do(req) 267 | if resp != nil { 268 | resp.Body.Close() 269 | } 270 | 271 | if err != nil { 272 | return err 273 | } 274 | 275 | if resp.StatusCode != http.StatusOK { 276 | return fmt.Errorf("cannot cancel %d: %d", orderid, resp.StatusCode) 277 | } 278 | 279 | return nil 280 | } 281 | -------------------------------------------------------------------------------- /profile.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strconv" 10 | ) 11 | 12 | const ( 13 | PrivacyStatePrivate = 1 14 | PrivacyStateFriendsOnly = 2 15 | PrivacyStatePublic = 3 16 | ) 17 | 18 | const ( 19 | CommentSettingSelf = "commentselfonly" 20 | CommentSettingFriends = "commentfriendsonly" 21 | CommentSettingPublic = "commentanyone" 22 | ) 23 | 24 | const ( 25 | apiGetPlayerSummaries = "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?" 26 | apiGetOwnedGames = "https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?" 27 | apiGetPlayerBans = "https://api.steampowered.com/ISteamUser/GetPlayerBans/v1/?" 28 | apiGetPlayerFriends = "https://api.steampowered.com/ISteamUser/GetFriendList/v1/?" 29 | apiResolveVanityURL = "https://api.steampowered.com/ISteamUser/ResolveVanityURL/v1/?" 30 | ) 31 | 32 | var ErrCannotFindVanityMatch = errors.New("no match for the vanity URL") 33 | 34 | type PlayerSummary struct { 35 | SteamID SteamID `json:"steamid,string"` 36 | VisibilityState uint32 `json:"communityvisibilitystate"` 37 | ProfileState uint32 `json:"profilestate"` 38 | PersonaName string `json:"personaname"` 39 | PersonaState uint32 `json:"personastate"` 40 | PersonaStateFlags uint32 `json:"personastateflags"` 41 | RealName string `json:"realname"` 42 | LastLogoff int64 `json:"lastlogoff"` 43 | ProfileURL string `json:"profileurl"` 44 | AvatarURL string `json:"avatar"` 45 | AvatarMediumURL string `json:"avatarmedium"` 46 | AvatarFullURL string `json:"avatarfull"` 47 | PrimaryClanID uint64 `json:"primaryclanid,string"` 48 | TimeCreated int64 `json:"timecreated"` 49 | LocCountryCode string `json:"loccountrycode"` 50 | LocStateCode string `json:"locstatecode"` 51 | LocCityID uint32 `json:"loccityid"` 52 | GameID uint64 `json:"gameid,string"` 53 | GameServerIP string `json:"gameserverip"` 54 | GameExtraInfo string `json:"gameextrainfo"` 55 | } 56 | 57 | type Game struct { 58 | AppID uint32 `json:"appid"` 59 | PlaytimeForever int64 `json:"playtime_forever"` 60 | Playtime2Weeks int64 `json:"playtime_2weeks"` 61 | } 62 | 63 | type OwnedGamesResponse struct { 64 | Count uint32 `json:"game_count"` 65 | Games []*Game `json:"games"` 66 | } 67 | 68 | type PlayerBan struct { 69 | SteamID uint64 `json:"SteamId,string"` 70 | CommunityBanned bool `json:"CommunityBanned"` 71 | VACBanned bool `json:"VACBanned"` 72 | NumberOfVACBans int `json:"NumberOfVACBans"` 73 | DaysSinceLastBan int `json:"DaysSinceLastBan"` 74 | NumberOfGameBans int `json:"NumberOfGameBans"` 75 | EconomyBan string `json:"EconomyBan"` 76 | } 77 | 78 | type Friend struct { 79 | SteamID uint64 `json:"steamid,string"` 80 | Relationship string `json:"relationship"` 81 | FriendSince int64 `json:"friend_since"` 82 | } 83 | 84 | func (session *Session) GetProfileURL() (string, error) { 85 | tmpClient := http.Client{Jar: session.client.Jar} 86 | 87 | /* We do not follow redirect, we want to know where it'd redirect us. */ 88 | tmpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { 89 | return errors.New("do not redirect") 90 | } 91 | 92 | /* Query normal, this will redirect us. */ 93 | resp, err := tmpClient.Get("https://steamcommunity.com/my") 94 | if resp == nil { 95 | return "", err 96 | } 97 | 98 | resp.Body.Close() 99 | if resp.StatusCode != http.StatusFound { 100 | return "", fmt.Errorf("http error: %d", resp.StatusCode) 101 | } 102 | 103 | /* We now have a few useful variables in header, for now, we will just grap "Location". */ 104 | return resp.Header.Get("Location"), nil 105 | } 106 | 107 | func (session *Session) SetupProfile(profileURL string) error { 108 | resp, err := session.client.Get(profileURL + "/edit?welcomed=1") 109 | if resp != nil { 110 | resp.Body.Close() 111 | } 112 | 113 | if err != nil { 114 | return err 115 | } 116 | 117 | if resp.StatusCode != http.StatusOK { 118 | return fmt.Errorf("http error: %d", resp.StatusCode) 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (session *Session) SetProfileInfo(profileURL string, values *map[string][]string) error { 125 | (*values)["sessionID"] = []string{session.sessionID} 126 | (*values)["type"] = []string{"profileSave"} 127 | 128 | resp, err := session.client.PostForm(profileURL+"/edit", *values) 129 | if resp != nil { 130 | resp.Body.Close() 131 | } 132 | 133 | if err != nil { 134 | return err 135 | } 136 | 137 | if resp.StatusCode != http.StatusOK { 138 | return fmt.Errorf("http error: %d", resp.StatusCode) 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func (session *Session) SetProfilePrivacy(profileURL string, commentPrivacy string, privacy uint8) error { 145 | resp, err := session.client.PostForm(profileURL+"/edit/settings", url.Values{ 146 | "sessionID": {session.sessionID}, 147 | "type": {"profileSettings"}, 148 | "commentSetting": {commentPrivacy}, 149 | "privacySetting": {strconv.FormatUint(uint64(privacy&0x3), 10)}, 150 | "inventoryPrivacySetting": {strconv.FormatUint(uint64((privacy>>2)&0x3), 10)}, 151 | "inventoryGiftPrivacy": {strconv.FormatUint(uint64((privacy>>4)&0x3), 10)}, 152 | }) 153 | if resp != nil { 154 | resp.Body.Close() 155 | } 156 | 157 | if err != nil { 158 | return err 159 | } 160 | 161 | if resp.StatusCode != http.StatusOK { 162 | return fmt.Errorf("http error: %d", resp.StatusCode) 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func (session *Session) GetPlayerSummaries(steamids string) ([]*PlayerSummary, error) { 169 | resp, err := session.client.Get(apiGetPlayerSummaries + url.Values{ 170 | "key": {session.apiKey}, 171 | "steamids": {steamids}, 172 | }.Encode()) 173 | if resp != nil { 174 | defer resp.Body.Close() 175 | } 176 | 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | type Players struct { 182 | Summaries []*PlayerSummary `json:"players"` 183 | } 184 | 185 | type Response struct { 186 | Inner Players `json:"response"` 187 | } 188 | 189 | var response Response 190 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 191 | return nil, err 192 | } 193 | 194 | return response.Inner.Summaries, nil 195 | } 196 | 197 | func (session *Session) GetOwnedGames(sid SteamID, freeGames bool, appInfo bool) (*OwnedGamesResponse, error) { 198 | resp, err := session.client.Get(apiGetOwnedGames + url.Values{ 199 | "key": {session.apiKey}, 200 | "steamid": {sid.ToString()}, 201 | "format": {"json"}, 202 | "include_appinfo": {strconv.FormatBool(appInfo)}, 203 | "include_played_free_games": {strconv.FormatBool(freeGames)}, 204 | }.Encode()) 205 | if resp != nil { 206 | defer resp.Body.Close() 207 | } 208 | 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | type Response struct { 214 | Inner *OwnedGamesResponse `json:"response"` 215 | } 216 | 217 | var response Response 218 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 219 | return nil, err 220 | } 221 | 222 | return response.Inner, nil 223 | } 224 | 225 | func (session *Session) GetPlayerBans(steamids string) ([]*PlayerBan, error) { 226 | resp, err := session.client.Get(apiGetPlayerBans + url.Values{ 227 | "key": {session.apiKey}, 228 | "steamids": {steamids}, 229 | }.Encode()) 230 | if resp != nil { 231 | defer resp.Body.Close() 232 | } 233 | 234 | if err != nil { 235 | return nil, err 236 | } 237 | 238 | type Response struct { 239 | Inner []*PlayerBan `json:"players"` 240 | } 241 | 242 | var response Response 243 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 244 | return nil, err 245 | } 246 | 247 | return response.Inner, nil 248 | } 249 | 250 | func (session *Session) GetFriends(sid SteamID) ([]*Friend, error) { 251 | resp, err := session.client.Get(apiGetPlayerFriends + url.Values{ 252 | "key": {session.apiKey}, 253 | "steamid": {sid.ToString()}, 254 | "format": {"json"}, 255 | }.Encode()) 256 | if resp != nil { 257 | defer resp.Body.Close() 258 | } 259 | 260 | if err != nil { 261 | return nil, err 262 | } 263 | 264 | type Friends struct { 265 | Friends []*Friend `json:"friends"` 266 | } 267 | 268 | type FriendsList struct { 269 | Inner Friends `json:"friendslist"` 270 | } 271 | 272 | var friendsList FriendsList 273 | if err = json.NewDecoder(resp.Body).Decode(&friendsList); err != nil { 274 | return nil, err 275 | } 276 | 277 | return friendsList.Inner.Friends, nil 278 | } 279 | 280 | func (session *Session) ResolveVanityURL(vanityURL string) (uint64, error) { 281 | resp, err := session.client.Get(apiResolveVanityURL + url.Values{ 282 | "key": {session.apiKey}, 283 | "vanityurl": {vanityURL}, 284 | }.Encode()) 285 | if resp != nil { 286 | defer resp.Body.Close() 287 | } 288 | 289 | if err != nil { 290 | return 0, err 291 | } 292 | 293 | type VanityData struct { 294 | Success uint32 `json:"success"` 295 | SteamID uint64 `json:"steamid,string"` 296 | } 297 | 298 | type Response struct { 299 | Inner VanityData `json:"response"` 300 | } 301 | 302 | var response Response 303 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 304 | return 0, err 305 | } 306 | 307 | if response.Inner.Success != 1 { 308 | return 0, ErrCannotFindVanityMatch 309 | } 310 | 311 | return response.Inner.SteamID, nil 312 | } 313 | -------------------------------------------------------------------------------- /steamapps.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | apiUpToDateCheck = "https://api.steampowered.com/ISteamApps/UpToDateCheck/v1?" 11 | ) 12 | 13 | func (session *Session) GetRequiredSteamAppVersion(appID int) (int, error) { 14 | resp, err := session.client.Get(apiUpToDateCheck + url.Values{ 15 | "appid": {strconv.Itoa(appID)}, 16 | "version": {"0"}, 17 | }.Encode()) 18 | if resp != nil { 19 | defer resp.Body.Close() 20 | } 21 | 22 | if err != nil { 23 | return 0, err 24 | } 25 | 26 | type UpToDateCheckResponse struct { 27 | RequiredVersion int `json:"required_version"` 28 | } 29 | 30 | type Response struct { 31 | Inner UpToDateCheckResponse `json:"response"` 32 | } 33 | 34 | var response Response 35 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 36 | return 0, err 37 | } 38 | return response.Inner.RequiredVersion, nil 39 | } 40 | -------------------------------------------------------------------------------- /steamid.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | ) 9 | 10 | const ( 11 | UniverseInvalid = iota 12 | UniversePublic 13 | UniverseBeta 14 | UniverseInternal 15 | UniverseDev 16 | ) 17 | 18 | const ( 19 | AccountTypeInvalid = iota 20 | AccountTypeIndividual 21 | AccountTypeMultiSeat 22 | AccountTypeGameServer 23 | AccountTypeAnonymousGameServer 24 | AccountTypePending 25 | AccountTypeContentServer 26 | AccountTypeClan 27 | AccountTypeChat 28 | AccountTypeP2PSuperSeeder 29 | AccountTypeAnonymous 30 | ) 31 | 32 | const ( 33 | AccountInstanceAll = iota 34 | AccountInstanceDesktop 35 | AccountInstanceConsole 36 | AccountInstanceWeb 37 | ) 38 | 39 | const ( 40 | ChatInstanceFlagClan = 0x80000 41 | ChatInstanceFlagLobby = 0x40000 42 | ChatInstanceFlagMMSLobby = 0x20000 43 | ) 44 | 45 | var ( 46 | // See Steam Documentation for more information on how this is formatted. 47 | // STEAM_X:Y:Z 48 | // X = universe (if 0 then this is universe public aka 1) 49 | // Y = lowest bit of Account ID 50 | // Z = upper 31 bits of Account ID 51 | legacyRegexp = regexp.MustCompile("STEAM_([0-5]):([0-1]):([0-9]+)") 52 | 53 | // Modern Steam ID 54 | // [C:U:A] or [C:U:A:I] 55 | // C = account type or instance ID 56 | // U = universe 57 | // A = account ID 58 | // I = instance id if not present, the default instance id for that C value is used. 59 | modernRegexp = regexp.MustCompile("\\[([a-zA-Z]):([0-5]):([0-9]+)(:[0-9]+)?\\]") 60 | 61 | ErrInvalidSteam2ID = errors.New("invalid input specified for a Steam 2 ID") 62 | ErrInvalidSteam3ID = errors.New("invalid input specified for a Steam 3 ID") 63 | ) 64 | 65 | /* 66 | * Full Steam 64-bit ID 67 | * Upper 32 bits Lower 32 bits 68 | * Upper 16 bits Lower 16 bits 69 | * Universe Type Acc Instance Account ID 70 | * |||| |||| xxxx |||| xxxx xx|| |||| |||| |||| |||| |||| |||| |||| |||| |||| |||| 71 | */ 72 | type SteamID uint64 73 | 74 | func (sid *SteamID) Parse(accid uint32, instance uint32, accountType uint32, universe uint8) { 75 | *sid = SteamID(uint64(accid) | (uint64(instance&0xFFFFF) << 32) | (uint64(accountType&0xF) << 52) | (uint64(universe) << 56)) 76 | } 77 | 78 | func (sid *SteamID) ParseDefaults(accid uint32) { 79 | sid.Parse(accid, AccountInstanceDesktop, AccountTypeIndividual, UniversePublic) 80 | } 81 | 82 | func (sid *SteamID) ParseSteam2ID(input string) error { 83 | m := legacyRegexp.FindStringSubmatch(input) 84 | if m == nil || len(m) < 4 { 85 | return ErrInvalidSteam2ID 86 | } 87 | 88 | universe, _ := strconv.ParseUint(string(m[1]), 10, 8) 89 | lobit, _ := strconv.ParseUint(string(m[2]), 10, 8) 90 | hibits, _ := strconv.ParseUint(string(m[3]), 10, 32) 91 | 92 | if universe == 0 { 93 | universe = 1 94 | } 95 | 96 | sid.Parse(uint32(lobit|hibits<<1), AccountInstanceDesktop, AccountTypeIndividual, uint8(universe)) 97 | return nil 98 | } 99 | 100 | func (sid *SteamID) ParseSteam3ID(input string) error { 101 | m := modernRegexp.FindStringSubmatch(input) 102 | if m == nil || len(m) < 4 { 103 | return ErrInvalidSteam3ID 104 | } 105 | 106 | accountID, _ := strconv.ParseUint(string(m[3]), 10, 32) 107 | universe, _ := strconv.ParseUint(string(m[2]), 10, 8) 108 | 109 | instance := uint64(AccountInstanceDesktop) 110 | if len(m) > 5 { 111 | instance, _ = strconv.ParseUint(string(m[5]), 10, 32) 112 | } 113 | 114 | accountType := uint32(AccountTypeIndividual) 115 | switch m[1][0] { 116 | case 'c': 117 | instance |= ChatInstanceFlagClan 118 | accountType = AccountTypeChat 119 | case 'L': 120 | instance |= ChatInstanceFlagLobby 121 | fallthrough 122 | case 'T': 123 | accountType = AccountTypeChat 124 | case 'I': 125 | accountType = AccountTypeInvalid 126 | case 'M': 127 | accountType = AccountTypeMultiSeat 128 | case 'G': 129 | accountType = AccountTypeGameServer 130 | case 'A': 131 | accountType = AccountTypeAnonymousGameServer 132 | case 'P': 133 | accountType = AccountTypePending 134 | case 'C': 135 | accountType = AccountTypeContentServer 136 | case 'g': 137 | accountType = AccountTypeClan 138 | case 'a': 139 | accountType = AccountTypeAnonymous 140 | } 141 | 142 | sid.Parse(uint32(accountID), uint32(instance), accountType, uint8(universe)) 143 | return nil 144 | } 145 | 146 | func (sid *SteamID) GetAccountID() uint32 { 147 | return uint32(*sid) 148 | } 149 | 150 | func (sid *SteamID) GetAccountInstance() uint32 { 151 | return uint32((*sid >> 32) & 0xFFFFF) 152 | } 153 | 154 | func (sid *SteamID) GetAccountType() uint32 { 155 | return uint32((*sid >> 52) & 0xF) 156 | } 157 | 158 | func (sid *SteamID) GetAccountUniverse() uint32 { 159 | return uint32((*sid >> 56) & 0xFF) 160 | } 161 | 162 | func (sid *SteamID) ToString() string { 163 | return strconv.FormatUint(uint64(*sid), 10) 164 | } 165 | 166 | func (sid *SteamID) ToSteam2ID() string { 167 | universe := sid.GetAccountUniverse() 168 | accountID := sid.GetAccountID() 169 | 170 | if universe == 1 { 171 | universe = 0 172 | } 173 | 174 | return fmt.Sprintf("STEAM_%d:%d:%d", universe, accountID&1, accountID>>1) 175 | } 176 | 177 | func (sid *SteamID) ToSteam3ID() string { 178 | accountTypeChar := 'I' 179 | instance := sid.GetAccountInstance() 180 | doInstance := false 181 | switch sid.GetAccountType() { 182 | case AccountTypeChat: 183 | if (instance & ChatInstanceFlagLobby) == ChatInstanceFlagLobby { 184 | accountTypeChar = 'L' 185 | } else if (instance & ChatInstanceFlagClan) == ChatInstanceFlagClan { 186 | accountTypeChar = 'c' 187 | } else { 188 | accountTypeChar = 'T' 189 | } 190 | case AccountTypeMultiSeat: 191 | accountTypeChar = 'M' 192 | doInstance = true 193 | case AccountTypeGameServer: 194 | accountTypeChar = 'G' 195 | case AccountTypeAnonymousGameServer: 196 | accountTypeChar = 'A' 197 | doInstance = true 198 | case AccountTypePending: 199 | accountTypeChar = 'P' 200 | case AccountTypeContentServer: 201 | accountTypeChar = 'C' 202 | case AccountTypeClan: 203 | accountTypeChar = 'g' 204 | case AccountTypeAnonymous: 205 | accountTypeChar = 'a' 206 | case AccountTypeIndividual: 207 | accountTypeChar = 'U' 208 | doInstance = instance != AccountInstanceDesktop 209 | } 210 | 211 | if doInstance { 212 | return fmt.Sprintf("[%c:%d:%d:%d]", accountTypeChar, sid.GetAccountUniverse(), sid.GetAccountID(), instance) 213 | } 214 | 215 | return fmt.Sprintf("[%c:%d:%d]", accountTypeChar, sid.GetAccountUniverse(), sid.GetAccountID()) 216 | } 217 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | var ErrInvalidPhoneNumber = errors.New("invalid phone number specified") 12 | 13 | type PhoneAPIResponse struct { 14 | Success bool `json:"success"` 15 | State string `json:"state"` 16 | ErrorText string `json:"errorText"` 17 | } 18 | 19 | func (session *Session) PrepareForSteamStore() { 20 | commu, _ := url.Parse("https://steamcommunity.com") 21 | store, _ := url.Parse("https://store.steampowered.com") 22 | 23 | session.client.Jar.SetCookies(store, session.client.Jar.Cookies(commu)) 24 | } 25 | 26 | func (session *Session) ValidatePhoneNumber(number string) error { 27 | resp, err := session.client.Get("https://store.steampowered.com/phone/validate?phoneNumber=" + url.QueryEscape(number)) 28 | if resp != nil { 29 | defer resp.Body.Close() 30 | } 31 | 32 | if err != nil { 33 | return err 34 | } 35 | 36 | var response PhoneAPIResponse 37 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 38 | return err 39 | } 40 | 41 | if !response.Success { 42 | return ErrInvalidPhoneNumber 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (session *Session) AddPhoneNumber(number string) error { 49 | resp, err := session.client.Get("https://store.steampowered.com/phone/add_ajaxop?" + url.Values{ 50 | "op": {"get_phone_number"}, 51 | "input": {number}, 52 | "sessionID": {session.sessionID}, 53 | "confirmed": {"0"}, 54 | }.Encode()) 55 | if resp != nil { 56 | defer resp.Body.Close() 57 | } 58 | 59 | if err != nil { 60 | return err 61 | } 62 | 63 | var response PhoneAPIResponse 64 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 65 | return err 66 | } 67 | 68 | if response.State != "get_sms_code" { 69 | return errors.New(response.ErrorText) 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func (session *Session) InitiateRemovePhoneNumber() error { 76 | resp, err := session.client.PostForm("https://store.steampowered.com/phone/remove_confirm_sms", url.Values{ 77 | "sessionID": {session.sessionID}, 78 | "bWasEdit": {""}, 79 | }) 80 | if resp != nil { 81 | defer resp.Body.Close() 82 | } 83 | 84 | if err != nil { 85 | return err 86 | } 87 | 88 | if resp.StatusCode != http.StatusOK { 89 | return fmt.Errorf("http error: %d", resp.StatusCode) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (session *Session) ConfirmRemovePhoneNumber(mobileCode string) error { 96 | resp, err := session.client.PostForm("https://store.steampowered.com/phone/remove_confirm_smscode_entry", url.Values{ 97 | "sessionID": {session.sessionID}, 98 | "bWasEdit": {""}, 99 | "smscode": {mobileCode}, 100 | }) 101 | if resp != nil { 102 | defer resp.Body.Close() 103 | } 104 | 105 | // FIXME: Make a regexp for error. 106 | if err != nil { 107 | return err 108 | } 109 | 110 | if resp.StatusCode != http.StatusOK { 111 | return fmt.Errorf("http error: %d", resp.StatusCode) 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func (session *Session) ReSendVerificationCode() error { 118 | resp, err := session.client.Get("https://store.steampowered.com/phone/add_ajaxop?" + url.Values{ 119 | "op": {"resend_sms"}, 120 | "input": {""}, 121 | "sessionID": {session.sessionID}, 122 | "confirmed": {"0"}, 123 | }.Encode()) 124 | if resp != nil { 125 | defer resp.Body.Close() 126 | } 127 | 128 | var response PhoneAPIResponse 129 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 130 | return err 131 | } 132 | 133 | if !response.Success { 134 | return errors.New(response.ErrorText) 135 | } 136 | 137 | if response.State != "get_sms_code" { 138 | return fmt.Errorf("unknown state: %s", response.State) 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func (session *Session) VerifyPhoneNumber(code string) error { 145 | resp, err := session.client.Get("https://store.steampowered.com/phone/add_ajaxop?" + url.Values{ 146 | "op": {"get_sms_code"}, 147 | "input": {code}, 148 | "sessionID": {session.sessionID}, 149 | "confirmed": {"0"}, 150 | }.Encode()) 151 | if resp != nil { 152 | defer resp.Body.Close() 153 | } 154 | 155 | if err != nil { 156 | return err 157 | } 158 | 159 | var response PhoneAPIResponse 160 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 161 | return err 162 | } 163 | 164 | if response.State != "done" { 165 | return errors.New(response.ErrorText) 166 | } 167 | 168 | return nil 169 | } 170 | -------------------------------------------------------------------------------- /totp.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "encoding/binary" 8 | "encoding/json" 9 | "net/http" 10 | ) 11 | 12 | const ( 13 | chars = "23456789BCDFGHJKMNPQRTVWXY" 14 | charsLen = uint32(len(chars)) 15 | ) 16 | 17 | type ServerTimeTip struct { 18 | Time int64 `json:"server_time,string"` 19 | SkewToleranceSeconds uint32 `json:"skew_tolerance_seconds,string"` 20 | LargeTimeJink uint32 `json:"large_time_jink,string"` 21 | ProbeFrequencySeconds uint32 `json:"probe_frequency_seconds"` 22 | AdjustedTimeProbeFrequencySeconds uint32 `json:"adjusted_time_probe_frequency_seconds"` 23 | HintProbeFrequencySeconds uint32 `json:"hint_probe_frequency_seconds"` 24 | SyncTimeout uint32 `json:"sync_timeout"` 25 | TryAgainSeconds uint32 `json:"try_again_seconds"` 26 | MaxAttempts uint32 `json:"max_attempts"` 27 | } 28 | 29 | func GenerateTwoFactorCode(sharedSecret string, current int64) (string, error) { 30 | data, err := base64.StdEncoding.DecodeString(sharedSecret) 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | ful := make([]byte, 8) 36 | binary.BigEndian.PutUint32(ful[4:], uint32(current/30)) 37 | 38 | hmac := hmac.New(sha1.New, data) 39 | hmac.Write(ful) 40 | 41 | sum := hmac.Sum(nil) 42 | start := sum[19] & 0x0F 43 | slice := binary.BigEndian.Uint32(sum[start:start+4]) & 0x7FFFFFFF 44 | 45 | buf := make([]byte, 5) 46 | for i := 0; i < 5; i++ { 47 | buf[i] = chars[slice%charsLen] 48 | slice /= charsLen 49 | } 50 | return string(buf), nil 51 | } 52 | 53 | func GenerateConfirmationCode(identitySecret, tag string, current int64) (string, error) { 54 | data, err := base64.StdEncoding.DecodeString(identitySecret) 55 | if err != nil { 56 | return "", err 57 | } 58 | 59 | ful := make([]byte, 8+len(tag)) 60 | binary.BigEndian.PutUint32(ful[4:], uint32(current)) 61 | copy(ful[8:], tag) 62 | 63 | hmac := hmac.New(sha1.New, data) 64 | hmac.Write(ful) 65 | 66 | return base64.StdEncoding.EncodeToString(hmac.Sum(nil)), nil 67 | } 68 | 69 | func GetTimeTip() (*ServerTimeTip, error) { 70 | resp, err := http.Post("https://api.steampowered.com/ITwoFactorService/QueryTime/v1/", "application/x-www-form-urlencoded", nil) 71 | if resp != nil { 72 | defer resp.Body.Close() 73 | } 74 | 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | type Response struct { 80 | Inner *ServerTimeTip `json:"response"` 81 | } 82 | 83 | var response Response 84 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 85 | return nil, err 86 | } 87 | 88 | return response.Inner, nil 89 | } 90 | -------------------------------------------------------------------------------- /tradeoffer.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const ( 17 | TradeStateNone = iota 18 | TradeStateInvalid 19 | TradeStateActive 20 | TradeStateAccepted 21 | TradeStateCountered 22 | TradeStateExpired 23 | TradeStateCanceled 24 | TradeStateDeclined 25 | TradeStateInvalidItems 26 | TradeStateCreatedNeedsConfirmation 27 | TradeStateCanceledByTwoFactor 28 | TradeStateInEscrow 29 | ) 30 | 31 | const ( 32 | TradeConfirmationNone = iota 33 | TradeConfirmationEmail 34 | TradeConfirmationMobileApp 35 | TradeConfirmationMobile 36 | ) 37 | 38 | const ( 39 | TradeFilterNone = iota 40 | TradeFilterSentOffers = 1 << 0 41 | TradeFilterRecvOffers = 1 << 1 42 | TradeFilterActiveOnly = 1 << 3 43 | TradeFilterHistoricalOnly = 1 << 4 44 | TradeFilterItemDescriptions = 1 << 5 45 | ) 46 | 47 | var ( 48 | // receiptExp matches JSON in the following form: 49 | // oItem = {"id":"...",...}; (Javascript code) 50 | receiptExp = regexp.MustCompile("oItem =\\s(.+?});") 51 | myEscrowExp = regexp.MustCompile("var g_daysMyEscrow = (\\d+);") 52 | themEscrowExp = regexp.MustCompile("var g_daysTheirEscrow = (\\d+);") 53 | errorMsgExp = regexp.MustCompile("
\\s*([^<]+)\\s*
") 54 | offerInfoExp = regexp.MustCompile("token=([a-zA-Z0-9-_]+)") 55 | 56 | apiGetTradeOffer = "https://api.steampowered.com/IEconService/GetTradeOffer/v1/?" 57 | apiGetTradeOffers = "https://api.steampowered.com/IEconService/GetTradeOffers/v1/?" 58 | apiDeclineTradeOffer = "https://api.steampowered.com/IEconService/DeclineTradeOffer/v1/" 59 | apiCancelTradeOffer = "https://api.steampowered.com/IEconService/CancelTradeOffer/v1/" 60 | 61 | ErrReceiptMatch = errors.New("unable to match items in trade receipt") 62 | ErrCannotAcceptActive = errors.New("unable to accept a non-active trade") 63 | ErrCannotFindOfferInfo = errors.New("unable to match data from trade offer url") 64 | ) 65 | 66 | type EconItem struct { 67 | AssetID uint64 `json:"assetid,string,omitempty"` 68 | InstanceID uint64 `json:"instanceid,string,omitempty"` 69 | ClassID uint64 `json:"classid,string,omitempty"` 70 | AppID uint32 `json:"appid"` 71 | ContextID uint64 `json:"contextid,string"` 72 | Amount uint16 `json:"amount,string"` 73 | Missing bool `json:"missing,omitempty"` 74 | } 75 | 76 | type EconDesc struct { 77 | Type string `json:"type"` 78 | Value string `json:"value"` 79 | Color string `json:"color"` 80 | } 81 | 82 | type EconTag struct { 83 | InternalName string `json:"internal_name"` 84 | Name string `json:"name"` 85 | Category string `json:"category"` 86 | CategoryName string `json:"category_name"` 87 | } 88 | 89 | type EconAction struct { 90 | Link string `json:"link"` 91 | Name string `json:"name"` 92 | } 93 | 94 | type EconItemDesc struct { 95 | ClassID uint64 `json:"classid,string"` // for matching with EconItem 96 | InstanceID uint64 `json:"instanceid,string"` // for matching with EconItem 97 | Tradable int `json:"tradable"` 98 | BackgroundColor string `json:"background_color"` 99 | IconURL string `json:"icon_url"` 100 | IconLargeURL string `json:"icon_url_large"` 101 | IconDragURL string `json:"icon_drag_url"` 102 | Name string `json:"name"` 103 | NameColor string `json:"name_color"` 104 | MarketName string `json:"market_name"` 105 | MarketHashName string `json:"market_hash_name"` 106 | Comodity bool `json:"comodity"` 107 | Actions []*EconAction `json:"actions"` 108 | Tags []*EconTag `json:"tags"` 109 | Descriptions []*EconDesc `json:"descriptions"` 110 | } 111 | 112 | type TradeOffer struct { 113 | ID uint64 `json:"tradeofferid,string"` 114 | Partner uint32 `json:"accountid_other"` 115 | ReceiptID uint64 `json:"tradeid,string"` 116 | RecvItems []*EconItem `json:"items_to_receive"` 117 | SendItems []*EconItem `json:"items_to_give"` 118 | Message string `json:"message"` 119 | State uint8 `json:"trade_offer_state"` 120 | ConfirmationMethod uint8 `json:"confirmation_method"` 121 | Created int64 `json:"time_created"` 122 | Updated int64 `json:"time_updated"` 123 | Expires int64 `json:"expiration_time"` 124 | EscrowEndDate int64 `json:"escrow_end_date"` 125 | RealTime bool `json:"from_real_time_trade"` 126 | IsOurOffer bool `json:"is_our_offer"` 127 | } 128 | 129 | type TradeOfferResponse struct { 130 | Offer *TradeOffer `json:"offer"` // GetTradeOffer 131 | SentOffers []*TradeOffer `json:"trade_offers_sent"` // GetTradeOffers 132 | ReceivedOffers []*TradeOffer `json:"trade_offers_received"` // GetTradeOffers 133 | Descriptions []*EconItemDesc `json:"descriptions"` // GetTradeOffers 134 | } 135 | 136 | type APIResponse struct { 137 | Inner *TradeOfferResponse `json:"response"` 138 | } 139 | 140 | func (session *Session) GetTradeOffer(id uint64) (*TradeOffer, error) { 141 | resp, err := session.client.Get(apiGetTradeOffer + url.Values{ 142 | "key": {session.apiKey}, 143 | "tradeofferid": {strconv.FormatUint(id, 10)}, 144 | }.Encode()) 145 | if resp != nil { 146 | defer resp.Body.Close() 147 | } 148 | 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | var response APIResponse 154 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 155 | return nil, err 156 | } 157 | 158 | return response.Inner.Offer, nil 159 | } 160 | 161 | func testBit(bits uint32, bit uint32) bool { 162 | return (bits & bit) == bit 163 | } 164 | 165 | func (session *Session) GetTradeOffers(filter uint32, timeCutOff time.Time) (*TradeOfferResponse, error) { 166 | params := url.Values{ 167 | "key": {session.apiKey}, 168 | } 169 | if testBit(filter, TradeFilterSentOffers) { 170 | params.Set("get_sent_offers", "1") 171 | } 172 | 173 | if testBit(filter, TradeFilterRecvOffers) { 174 | params.Set("get_received_offers", "1") 175 | } 176 | 177 | if testBit(filter, TradeFilterActiveOnly) { 178 | params.Set("active_only", "1") 179 | } 180 | 181 | if testBit(filter, TradeFilterItemDescriptions) { 182 | params.Set("get_descriptions", "1") 183 | } 184 | 185 | if testBit(filter, TradeFilterHistoricalOnly) { 186 | params.Set("historical_only", "1") 187 | params.Set("time_historical_cutoff", strconv.FormatInt(timeCutOff.Unix(), 10)) 188 | } 189 | 190 | resp, err := session.client.Get(apiGetTradeOffers + params.Encode()) 191 | if resp != nil { 192 | defer resp.Body.Close() 193 | } 194 | 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | var response APIResponse 200 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 201 | return nil, err 202 | } 203 | 204 | return response.Inner, nil 205 | } 206 | 207 | func (session *Session) GetMyTradeToken() (string, error) { 208 | resp, err := session.client.Get("https://steamcommunity.com/my/tradeoffers/privacy") 209 | if resp != nil { 210 | defer resp.Body.Close() 211 | } 212 | 213 | if err != nil { 214 | return "", err 215 | } 216 | 217 | if resp.StatusCode != http.StatusOK { 218 | return "", fmt.Errorf("http error: %d", resp.StatusCode) 219 | } 220 | 221 | body, err := ioutil.ReadAll(resp.Body) 222 | if err != nil { 223 | return "", err 224 | } 225 | 226 | m := offerInfoExp.FindStringSubmatch(string(body)) 227 | if m == nil || len(m) != 2 { 228 | return "", ErrCannotFindOfferInfo 229 | } 230 | 231 | return string(m[1]), nil 232 | } 233 | 234 | type EscrowSteamGuardInfo struct { 235 | MyDays int64 236 | ThemDays int64 237 | ErrorMsg string 238 | } 239 | 240 | func (session *Session) GetEscrowGuardInfo(sid SteamID, token string) (*EscrowSteamGuardInfo, error) { 241 | resp, err := session.client.Get("https://steamcommunity.com/tradeoffer/new/?" + url.Values{ 242 | "partner": {strconv.FormatUint(uint64(sid.GetAccountID()), 10)}, 243 | "token": {token}, 244 | }.Encode()) 245 | if resp != nil { 246 | defer resp.Body.Close() 247 | } 248 | 249 | if err != nil { 250 | return nil, err 251 | } 252 | 253 | if resp.StatusCode != http.StatusOK { 254 | return nil, fmt.Errorf("http error: %d", resp.StatusCode) 255 | } 256 | 257 | body, err := ioutil.ReadAll(resp.Body) 258 | if err != nil { 259 | return nil, err 260 | } 261 | 262 | var my int64 263 | var them int64 264 | var errMsg string 265 | 266 | m := myEscrowExp.FindStringSubmatch(string(body)) 267 | if m != nil && len(m) == 2 { 268 | my, _ = strconv.ParseInt(m[1], 10, 32) 269 | } 270 | 271 | m = themEscrowExp.FindStringSubmatch(string(body)) 272 | if m != nil && len(m) == 2 { 273 | them, _ = strconv.ParseInt(m[1], 10, 32) 274 | } 275 | 276 | m = errorMsgExp.FindStringSubmatch(string(body)) 277 | if m != nil && len(m) == 2 { 278 | errMsg = string(m[1]) 279 | } 280 | 281 | return &EscrowSteamGuardInfo{ 282 | MyDays: my, 283 | ThemDays: them, 284 | ErrorMsg: errMsg, 285 | }, nil 286 | } 287 | 288 | func (session *Session) SendTradeOffer(offer *TradeOffer, sid SteamID, token string) error { 289 | content := map[string]interface{}{ 290 | "newversion": true, 291 | "version": 3, 292 | "me": map[string]interface{}{ 293 | "assets": offer.SendItems, 294 | "currency": make([]struct{}, 0), 295 | "ready": false, 296 | }, 297 | "them": map[string]interface{}{ 298 | "assets": offer.RecvItems, 299 | "currency": make([]struct{}, 0), 300 | "ready": false, 301 | }, 302 | } 303 | 304 | contentJSON, err := json.Marshal(content) 305 | if err != nil { 306 | return err 307 | } 308 | 309 | req, err := http.NewRequest( 310 | http.MethodPost, 311 | "https://steamcommunity.com/tradeoffer/new/send", 312 | strings.NewReader(url.Values{ 313 | "sessionid": {session.sessionID}, 314 | "serverid": {"1"}, 315 | "partner": {sid.ToString()}, 316 | "tradeoffermessage": {offer.Message}, 317 | "json_tradeoffer": {string(contentJSON)}, 318 | "trade_offer_create_params": {"{\"trade_offer_access_token\":\"" + token + "\"}"}, 319 | }.Encode()), 320 | ) 321 | if err != nil { 322 | return err 323 | } 324 | req.Header.Add("Referer", "https://steamcommunity.com/tradeoffer/new/?"+url.Values{ 325 | "partner": {strconv.FormatUint(uint64(sid.GetAccountID()), 10)}, 326 | "token": {token}, 327 | }.Encode()) 328 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 329 | 330 | resp, err := session.client.Do(req) 331 | if resp != nil { 332 | defer resp.Body.Close() 333 | } 334 | 335 | if err != nil { 336 | return err 337 | } 338 | 339 | type Response struct { 340 | ErrorMessage string `json:"strError"` 341 | ID uint64 `json:"tradeofferid,string"` 342 | MobileConfirmationRequired bool `json:"needs_mobile_confirmation"` 343 | EmailConfirmationRequired bool `json:"needs_email_confirmation"` 344 | EmailDomain string `json:"email_domain"` 345 | } 346 | 347 | var response Response 348 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 349 | return err 350 | } 351 | 352 | if len(response.ErrorMessage) != 0 { 353 | return errors.New(response.ErrorMessage) 354 | } 355 | 356 | if response.ID == 0 { 357 | return errors.New("no OfferID included") 358 | } 359 | 360 | offer.ID = response.ID 361 | offer.Created = time.Now().Unix() 362 | offer.Updated = time.Now().Unix() 363 | offer.Expires = offer.Created + 14*24*60*60 364 | offer.RealTime = false 365 | offer.IsOurOffer = true 366 | 367 | // Just test mobile confirmation, email is deprecated 368 | if response.MobileConfirmationRequired { 369 | offer.ConfirmationMethod = TradeConfirmationMobileApp 370 | offer.State = TradeStateCreatedNeedsConfirmation 371 | } else { 372 | // set state to active 373 | offer.State = TradeStateActive 374 | } 375 | 376 | return nil 377 | } 378 | 379 | func (session *Session) GetTradeReceivedItems(receiptID uint64) ([]*InventoryItem, error) { 380 | resp, err := session.client.Get(fmt.Sprintf("https://steamcommunity.com/trade/%d/receipt", receiptID)) 381 | if resp != nil { 382 | defer resp.Body.Close() 383 | } 384 | 385 | if err != nil { 386 | return nil, err 387 | } 388 | 389 | if resp.StatusCode != http.StatusOK { 390 | return nil, fmt.Errorf("http error: %d", resp.StatusCode) 391 | } 392 | 393 | body, err := ioutil.ReadAll(resp.Body) 394 | if err != nil { 395 | return nil, err 396 | } 397 | 398 | m := receiptExp.FindAllSubmatch(body, -1) 399 | if m == nil { 400 | return nil, ErrReceiptMatch 401 | } 402 | 403 | items := make([]*InventoryItem, len(m)) 404 | for k := range m { 405 | item := &InventoryItem{} 406 | if err = json.Unmarshal(m[k][1], item); err != nil { 407 | return nil, err 408 | } 409 | 410 | items[k] = item 411 | } 412 | 413 | return items, nil 414 | } 415 | 416 | func (session *Session) DeclineTradeOffer(id uint64) error { 417 | resp, err := session.client.PostForm(apiDeclineTradeOffer, url.Values{ 418 | "key": {session.apiKey}, 419 | "tradeofferid": {strconv.FormatUint(id, 10)}, 420 | }) 421 | if resp != nil { 422 | resp.Body.Close() 423 | } 424 | 425 | if err != nil { 426 | return err 427 | } 428 | 429 | result := resp.Header.Get("x-eresult") 430 | if result != "1" { 431 | return fmt.Errorf("cannot decline trade: %s", result) 432 | } 433 | 434 | return nil 435 | } 436 | 437 | func (session *Session) CancelTradeOffer(id uint64) error { 438 | resp, err := session.client.PostForm(apiCancelTradeOffer, url.Values{ 439 | "key": {session.apiKey}, 440 | "tradeofferid": {strconv.FormatUint(id, 10)}, 441 | }) 442 | if resp != nil { 443 | resp.Body.Close() 444 | } 445 | 446 | if err != nil { 447 | return err 448 | } 449 | 450 | result := resp.Header.Get("x-eresult") 451 | if result != "1" { 452 | return fmt.Errorf("cannot cancel trade: %s", result) 453 | } 454 | 455 | return nil 456 | } 457 | 458 | func (session *Session) AcceptTradeOffer(id uint64) error { 459 | tid := strconv.FormatUint(id, 10) 460 | postURL := "https://steamcommunity.com/tradeoffer/" + tid 461 | 462 | req, err := http.NewRequest( 463 | http.MethodPost, 464 | postURL+"/accept", 465 | strings.NewReader(url.Values{ 466 | "sessionid": {session.sessionID}, 467 | "serverid": {"1"}, 468 | "tradeofferid": {tid}, 469 | }.Encode()), 470 | ) 471 | if err != nil { 472 | return err 473 | } 474 | 475 | req.Header.Add("Referer", postURL) 476 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 477 | 478 | resp, err := session.client.Do(req) 479 | if resp != nil { 480 | defer resp.Body.Close() 481 | } 482 | 483 | if err != nil { 484 | return err 485 | } 486 | 487 | if resp.StatusCode != http.StatusOK { 488 | return fmt.Errorf("http error: %d", resp.StatusCode) 489 | } 490 | 491 | type Response struct { 492 | ErrorMessage string `json:"strError"` 493 | } 494 | 495 | var response Response 496 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 497 | return err 498 | } 499 | 500 | if len(response.ErrorMessage) != 0 { 501 | return errors.New(response.ErrorMessage) 502 | } 503 | 504 | return nil 505 | } 506 | 507 | func (offer *TradeOffer) Send(session *Session, sid SteamID, token string) error { 508 | return session.SendTradeOffer(offer, sid, token) 509 | } 510 | 511 | func (offer *TradeOffer) Accept(session *Session) error { 512 | return session.AcceptTradeOffer(offer.ID) 513 | } 514 | 515 | func (offer *TradeOffer) Cancel(session *Session) error { 516 | if offer.IsOurOffer { 517 | return session.CancelTradeOffer(offer.ID) 518 | } 519 | 520 | return session.DeclineTradeOffer(offer.ID) 521 | } 522 | -------------------------------------------------------------------------------- /twofactor.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/url" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | type TwoFactorInfo struct { 12 | Status uint32 `json:"status"` 13 | SharedSecret string `json:"shared_secret"` 14 | IdentitySecret string `json:"identity_secret"` 15 | Secret1 string `json:"secret_1"` 16 | SerialNumber uint64 `json:"serial_number,string"` 17 | RevocationCode string `json:"revocation_code"` 18 | URI string `json:"uri"` 19 | ServerTime uint64 `json:"server_time,string"` 20 | TokenGID string `json:"token_gid"` 21 | } 22 | 23 | type FinalizeTwoFactorInfo struct { 24 | Status uint32 `json:"status"` 25 | ServerTime uint64 `json:"server_time,string"` 26 | } 27 | 28 | const ( 29 | enableTwoFactorURL = "https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/" 30 | finalizeTwoFactorURL = "https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/" 31 | disableTwoFactorURL = "https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/" 32 | ) 33 | 34 | var ErrCannotDisable = errors.New("unable to process disable two factor request") 35 | 36 | func (session *Session) EnableTwoFactor() (*TwoFactorInfo, error) { 37 | resp, err := session.client.PostForm(enableTwoFactorURL, url.Values{ 38 | "steamid": {session.oauth.SteamID.ToString()}, 39 | "access_token": {session.oauth.Token}, 40 | "authenticator_time": {strconv.FormatInt(time.Now().Unix(), 10)}, 41 | "authenticator_type": {"1"}, /* 1 = Valve's, 2 = thirdparty */ 42 | "device_identifier": {session.deviceID}, 43 | "sms_phone_id": {"1"}, 44 | }) 45 | if resp != nil { 46 | defer resp.Body.Close() 47 | } 48 | 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | type Response struct { 54 | Inner *TwoFactorInfo `json:"response"` 55 | } 56 | 57 | var response Response 58 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 59 | return nil, err 60 | } 61 | 62 | return response.Inner, nil 63 | } 64 | 65 | func (session *Session) FinalizeTwoFactor(authCode, mobileCode string) (*FinalizeTwoFactorInfo, error) { 66 | resp, err := session.client.PostForm(finalizeTwoFactorURL, url.Values{ 67 | "steamid": {session.oauth.SteamID.ToString()}, 68 | "access_token": {session.oauth.Token}, 69 | "authenticator_time": {strconv.FormatInt(time.Now().Unix(), 10)}, 70 | "authenticator_code": {authCode}, 71 | "activation_code": {mobileCode}, 72 | }) 73 | if resp != nil { 74 | defer resp.Body.Close() 75 | } 76 | 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | type Response struct { 82 | Inner *FinalizeTwoFactorInfo `json:"response"` 83 | } 84 | 85 | var response Response 86 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 87 | return nil, err 88 | } 89 | 90 | return response.Inner, nil 91 | } 92 | 93 | func (session *Session) DisableTwoFactor(revocationCode string) error { 94 | resp, err := session.client.PostForm(disableTwoFactorURL, url.Values{ 95 | "steamid": {session.oauth.SteamID.ToString()}, 96 | "access_token": {session.oauth.Token}, 97 | "revocation_code": {revocationCode}, 98 | "steamguard_scheme": {"1"}, 99 | }) 100 | if resp != nil { 101 | defer resp.Body.Close() 102 | } 103 | 104 | if err != nil { 105 | return err 106 | } 107 | 108 | type Disabled struct { 109 | Success bool `json:"success"` 110 | } 111 | type Response struct { 112 | Inner *Disabled `json:"response"` 113 | } 114 | 115 | var response Response 116 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil { 117 | return err 118 | } 119 | 120 | if !response.Inner.Success { 121 | return ErrCannotDisable 122 | } 123 | 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /webapi.go: -------------------------------------------------------------------------------- 1 | package steam 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "regexp" 9 | ) 10 | 11 | const ( 12 | apiKeyURL = "https://steamcommunity.com/dev/apikey" 13 | apiKeyRegisterURL = "https://steamcommunity.com/dev/registerkey" 14 | apiKeyRevokeURL = "https://steamcommunity.com/dev/revokekey" 15 | 16 | accessDeniedPattern = "

Access Denied

" 17 | ) 18 | 19 | var ( 20 | keyRegExp = regexp.MustCompile("

Key: ([0-9A-F]+)

") 21 | 22 | ErrCannotRegisterKey = errors.New("unable to register API key") 23 | ErrCannotRevokeKey = errors.New("unable to revoke API key") 24 | ErrAccessDenied = errors.New("access is denied") 25 | ErrKeyNotFound = errors.New("key not found") 26 | ) 27 | 28 | func (session *Session) parseKey(resp *http.Response) (string, error) { 29 | body, err := ioutil.ReadAll(resp.Body) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | if m, err := regexp.Match(accessDeniedPattern, body); err != nil { 35 | return "", err 36 | } else if m { 37 | return "", ErrAccessDenied 38 | } 39 | 40 | submatch := keyRegExp.FindStringSubmatch(string(body)) 41 | if len(submatch) != 2 { 42 | return "", ErrKeyNotFound 43 | } 44 | 45 | session.apiKey = submatch[1] 46 | return submatch[1], nil 47 | } 48 | 49 | func (session *Session) RegisterWebAPIKey(domain string) (string, error) { 50 | resp, err := session.client.PostForm(apiKeyRegisterURL, url.Values{ 51 | "domain": {domain}, 52 | "agreeToTerms": {"agreed"}, 53 | "sessionid": {session.sessionID}, 54 | "Submit": {"Register"}, 55 | }) 56 | if resp != nil { 57 | defer resp.Body.Close() 58 | } 59 | 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | if resp.StatusCode != http.StatusOK { 65 | return "", ErrCannotRegisterKey 66 | } 67 | 68 | return session.parseKey(resp) 69 | } 70 | 71 | func (session *Session) GetWebAPIKey() (string, error) { 72 | resp, err := session.client.Get(apiKeyURL) 73 | if resp != nil { 74 | defer resp.Body.Close() 75 | } 76 | 77 | if err != nil { 78 | return "", err 79 | } 80 | 81 | return session.parseKey(resp) 82 | } 83 | 84 | func (session *Session) RevokeWebAPIKey() error { 85 | resp, err := session.client.PostForm(apiKeyRevokeURL, url.Values{ 86 | "Revoke": {"Revoke My Steam Web API Key"}, 87 | "sessionid": {session.sessionID}, 88 | }) 89 | if resp != nil { 90 | resp.Body.Close() 91 | } 92 | 93 | if err != nil { 94 | return err 95 | } 96 | 97 | if resp.StatusCode != http.StatusOK { 98 | return ErrCannotRevokeKey 99 | } 100 | 101 | return nil 102 | } 103 | --------------------------------------------------------------------------------