commit b59df00ca88a156022b9ab67ff9fdf29eb05ebde Author: Hannes Mannerheim Date: Mon Aug 19 15:30:57 2013 +0200 upload diff --git a/API.php b/API.php new file mode 100644 index 0000000..d81539c --- /dev/null +++ b/API.php @@ -0,0 +1,95 @@ + \\\\_\ · + · \\) \____) · + · · + · · + · · + · Qvitter is free software: you can redistribute it and / or modify it · + · under the terms of the GNU Affero General Public License as published by · + · the Free Software Foundation, either version three of the License or (at · + · your option) any later version. · + · · + · Qvitter is distributed in hope that it will be useful but WITHOUT ANY · + · WARRANTY; without even the implied warranty of MERCHANTABILTY or FITNESS · + · FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for · + · more details. · + · · + · You should have received a copy of the GNU Affero General Public License · + · along with Qvitter. If not, see . · + · · + · Contact h@nnesmannerhe.im if you have any questions. · + · · + · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · */ + +include 'settings.php'; + +header("Content-type: application/json; charset=utf-8"); +if(substr($apiroot,-1) != '/') { $apiroot .= '/'; } // add slash if missing + +// post requests +if(isset($_POST['postRequest'])) { + $query = http_build_query($_POST, '', '&'); + $ch=curl_init(); + curl_setopt($ch, CURLOPT_URL, $apiroot.urldecode($_POST['postRequest'])); + curl_setopt($ch, CURLOPT_USERPWD, $_POST['username'].":".$_POST['password']); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $query); + session_write_close(); // fix problem with curling to local + $reply=curl_exec($ch); + curl_close($ch); + session_start(); + + // force ssl on our domain + if($forcessl) { + $reply = str_replace('http://'.$siterootdomain,'https://'.$siterootdomain,$reply); + } + + print $reply; + } + +// get requests +elseif(isset($_POST['getRequest'])) { + $ch=curl_init(); + curl_setopt($ch, CURLOPT_URL, $apiroot.$_POST['getRequest']); + + if(isset($_POST['username'])) { + curl_setopt($ch, CURLOPT_USERPWD, $_POST['username'].":".$_POST['password']); + } + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + session_write_close(); + $reply=curl_exec($ch); + curl_close($ch); + session_start(); + + // force ssl on our domain + if($forcessl) { + $reply = str_replace('http:\/\/'.$siterootdomain,'https:\/\/'.$siterootdomain,$reply); + } + + print $reply; + } + + + + /* · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · + · · + · (o> >o) · + · \\\\_\ /_//// . + · \____) (____/ · + · · + · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · */?> \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..dba13ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8267e65 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +Qvitter +========================================== + +* Author: Hannes Mannerheim () +* Last mod.: August, 2013 +* Version: 1 +* Website: +* GitHub: + +Qvitter is free software: you can redistribute it and / or modify it +under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version three of the License or (at +your option) any later version. + +Qvitter is distributed in hope that it will be useful but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILTY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +more details. + +You should have received a copy of the GNU Affero General Public License +along with Qvitter. If not, see . + +Setup +----- + +You need a webserver with PHP support. + +Edit settings.php. + +(Qvitter uses a slightly modified statusnet API. Some things will not work +if you connect to a site with standard API. Files are included if you want +to Qvitter-mod your Statusnet API.) + + +TODO +---- + +1. Join new external groups and follow new external users + +2. Follow people on other instances + +3. Auto suggest mentions + +4. Register + +5. Background image uploading/editing + +6. Color theme + +7. Auto url-shortening setting under queet box + +10. Settings (e.g. don't show replies to people I don't follow) + +11. Syntax-coloring in queet-box, maybe codemirror (worked nicely for ltr but not rtl text when I tried it) + +12. Image/file upload, drag-n-drop! + +13. Search users + +14. Recommended users + +15. Filters (hide queets containing strings, e.g. mute users) + +18. Better responsive design + +19. More languages + +20. Queet-page + +21. New api for serving _number_ of new items in several streams (to show number of new items in menu/history) + +22. New "expand queet" api for getting conversation, retweets, favs and attachment in the same request + +23. DMs + +24. Node.js long polling server and an new api that serve aggregate of all polling users requests in one go diff --git a/api-changes-1.1.1/CHANGES b/api-changes-1.1.1/CHANGES new file mode 100644 index 0000000..82514aa --- /dev/null +++ b/api-changes-1.1.1/CHANGES @@ -0,0 +1,160 @@ + CHANGES TO API + -------------- + + * actions/apiattachment.php New api action + * actions/apistatusesfavs.php New api action + * actions/apicheckhub.php New api action (not used yet) + * actions/apiexternalprofileshow.php New api action + + * lib/apiaction.php + + - add urls to larger avatars + + ~LINE 213 $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE); + $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() : + Avatar::defaultImage(AVATAR_STREAM_SIZE); + $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + $twitter_user['profile_image_url_profile_size'] = ($avatar) ? $avatar->displayUrl() : + Avatar::defaultImage(AVATAR_PROFILE_SIZE); + $avatar = $profile->getOriginalAvatar(); + $twitter_user['profile_image_url_original'] = ($avatar) ? $avatar->displayUrl() : + Avatar::defaultImage(AVATAR_PROFILE_SIZE); + $groups = $profile->getGroups(); + $groups_count = 0; while($groups->fetch()) $groups_count++; + $twitter_user['groups_count'] = $groups_count; + + + - add the uri-field + + ~line 320 $twitter_status['uri'] = $notice->uri; + + + - show if a notices is favorited by auth_user + + ~line 345 if (isset($this->auth_user)) { + $this_profile = $this->auth_user->getProfile(); + $twitter_status['favorited'] = $this->auth_user->hasFave($notice); + $twitter_status['repeated'] = $this_profile->hasRepeated($notice->id); + } else { + $twitter_status['favorited'] = false; + $twitter_status['repeated'] = false; + } + + + - show number of admins in group api + + ~line 420 $admins = $group->getAdmins(); + $admin_count = 0; while($admins->fetch()) $admin_count++; + $twitter_group['admin_count'] = $admin_count; + + + - to be able to get group profile by uri. + - hackish though, because if uri get-var is sent, it will discard the id get var + - (but id still needs to be sent and be non-numerical, so I do "?id=foo&uri={uri}") + - should be possible to only supply uri get var, but I was lazy... sry + + ~line 1565 } else if ($this->arg('uri')) { + return User_group::staticGet('uri', urldecode($this->arg('uri'))); + + + * lib/router.php + + - add routing for new api actions + + ~line 467: $m->connect('api/statuses/favs/:id.json', + array('action' => 'ApiStatusesFavs', + 'id' => '[0-9]+')); + + $m->connect('api/attachment/:id.json', + array('action' => 'ApiAttachment', + 'id' => '[0-9]+')); + + $m->connect('api/checkhub.json', + array('action' => 'ApiCheckHub')); + + $m->connect('api/externalprofile/show.json', + array('action' => 'ApiExternalProfileShow')); + + + - also, tags need regexp to work with unicode charachters, e.g. farsi and arabic: + + $m->connect('api/statusnet/tags/timeline/:tag.:format', + array('action' => 'ApiTimelineTag', + 'tag' => self::REGEX_TAG, + 'format' => '(xml|json|rss|atom|as)')); + + + * acitons/apiconversation.php + + - I didn't always get Profile::current() to show me the auth user's profile, so I changed it to the normal $this->auth_user used in other api actions + + ~ line 80: if(isset($this->auth_user)) { + $profile = $this->auth_user->getProfile(); + } + else { + $profile = null; + } + + + + + *actions/apitimelineuser.php + + - this api did only return the public user timeline, not the auth user's. + - e.g. it did not show notices from people who post to "my colleques at quitter" + - changed to return timeline according to which auth user is requesting + + ~ line 238 $user_profile = $this->user->getProfile(); + if(isset($this->auth_user)) { + $auth_user_profile = $this->auth_user->getProfile(); + } + else { + $auth_user_profile = null; + } + + $stream = new ProfileNoticeStream($user_profile, $auth_user_profile); + + $notice = $stream->getNotices(($this->page-1) * $this->count, + $this->count + 1, + $this->since_id, + $this->max_id); + + + * search.json + + - changed response to normal twitter format, maybe I should have created a new api action for that, + - but... i don't see the point in having a special format for searches, it should be same as other streams + + + + * actions/timelinetags.php + + - added max_id and since_id + + ~line 179 $notice = Notice_tag::getStream( + $this->tag, + ($this->page - 1) * $this->count, + $this->count + 1, + $this->since_id, + $this->max_id + ); + + * actions/apistatusesupdate.php + + - we don't want statuses to shorten if sent through the api + + ~ line 290: //$status_shortened = $this->auth_user->shortenlinks($this->status); + $status_shortened = $this->status; + + + * classes/Notice.php + + - to _not_ shorten urls sent through api, we need to comment out this also + + ~ line 352 // if ($user) { + // // Use the local user's shortening preferences, if applicable. + // $final = $user->shortenLinks($content); + // } else { + // $final = common_shorten_links($content); + // } + diff --git a/api-changes-1.1.1/actions/apiattachment.php b/api-changes-1.1.1/actions/apiattachment.php new file mode 100644 index 0000000..9e7cd40 --- /dev/null +++ b/api-changes-1.1.1/actions/apiattachment.php @@ -0,0 +1,106 @@ +. + * + * @category API + * @package StatusNet + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; +require_once INSTALLDIR . '/lib/mediafile.php'; + +/** + * Show a notice's attachment + * + */ +class ApiAttachmentAction extends ApiAuthAction +{ + const MAXCOUNT = 100; + + var $original = null; + var $cnt = self::MAXCOUNT; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + return true; + } + + /** + * Handle the request + * + * Make a new notice for the update, save it, and show it + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + $file = new File(); + $file->selectAdd(); // clears it + $file->selectAdd('url'); + $file->id = $this->trimmed('id'); + $url = $file->fetchAll('url'); + + $file_txt = ''; + if(strstr($url[0],'.html')) { + $file_txt['txt'] = file_get_contents(str_replace('://quitter.se','://127.0.0.1',$url[0])); + $file_txt['body_start'] = strpos($file_txt['txt'],'')+6; + $file_txt['body_end'] = strpos($file_txt['txt'],''); + $file_txt = substr($file_txt['txt'],$file_txt['body_start'],$file_txt['body_end']-$file_txt['body_start']); + } + + $this->initDocument('json'); + $this->showJsonObjects($file_txt); + $this->endDocument('json'); + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return true; + } +} diff --git a/api-changes-1.1.1/actions/apicheckhub.php b/api-changes-1.1.1/actions/apicheckhub.php new file mode 100644 index 0000000..7074afe --- /dev/null +++ b/api-changes-1.1.1/actions/apicheckhub.php @@ -0,0 +1,122 @@ +. + * + * @category API + * @package StatusNet + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; +require_once INSTALLDIR . '/lib/mediafile.php'; + +/** + * Check if a url have a push-hub, i.e. if it is possible to subscribe + * + */ +class ApiCheckHubAction extends ApiAuthAction +{ + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->url = urldecode($args['url']); + + if (!$this->url) { + $this->clientError(_('No URL.'), 403, 'json'); + return; + } + + if (!Validate::uri( + $this->url, array( + 'allowed_schemes' => + array('http', 'https') + ) + )) { + $this->clientError(_('Invalid URL.'), 403, 'json'); + return; + } + + return true; + } + + /** + * Handle the request + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + + $discover = new FeedDiscovery(); + + try { + $feeduri = $discover->discoverFromURL($this->url); + if($feeduri) { + $huburi = $discover->getHubLink(); + } + } catch (FeedSubNoFeedException $e) { + $this->clientError(_('No feed found'), 403, 'json'); + return; + } catch (FeedSubBadResponseException $e) { + $this->clientError(_('No hub found'), 403, 'json'); + return; + } + + $hub_status = array(); + if ($huburi) { + $hub_status = array('huburi' => $huburi); + } + + $this->initDocument('json'); + $this->showJsonObjects($hub_status); + $this->endDocument('json'); + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return true; + } +} diff --git a/api-changes-1.1.1/actions/apiconversation.php b/api-changes-1.1.1/actions/apiconversation.php new file mode 100644 index 0000000..86fbe93 --- /dev/null +++ b/api-changes-1.1.1/actions/apiconversation.php @@ -0,0 +1,244 @@ +. + * + * @category API + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Show a stream of notices in a particular conversation + * + * @category API + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class ApiconversationAction extends ApiAuthAction +{ + protected $conversation = null; + protected $notices = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $convId = $this->trimmed('id'); + + if (empty($convId)) { + // TRANS: Client exception thrown when no conversation ID is given. + throw new ClientException(_('No conversation ID.')); + } + + $this->conversation = Conversation::staticGet('id', $convId); + + if (empty($this->conversation)) { + // TRANS: Client exception thrown when referring to a non-existing conversation ID (%d). + throw new ClientException(sprintf(_('No conversation with ID %d.'), $convId), + 404); + } + +// $profile = Profile::current(); + if(isset($this->auth_user)) { + $profile = $this->auth_user->getProfile(); + } + else { + $profile = null; + } + + $stream = new ConversationNoticeStream($convId, $profile); + + $notice = $stream->getNotices(($this->page-1) * $this->count, + $this->count, + $this->since_id, + $this->max_id); + + $this->notices = $notice->fetchAll(); + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + $sitename = common_config('site', 'name'); + // TRANS: Title for conversion timeline. + $title = _m('TITLE', 'Conversation'); + $id = common_local_url('apiconversation', array('id' => $this->conversation->id, 'format' => $this->format)); + $link = common_local_url('conversation', array('id' => $this->conversation->id)); + + $self = $id; + + switch($this->format) { + case 'xml': + $this->showXmlTimeline($this->notices); + break; + case 'rss': + $this->showRssTimeline( + $this->notices, + $title, + $link, + null, + null, + null, + $self + ); + break; + case 'atom': + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed($this->auth_user); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setUpdated('now'); + + $atom->addLink($link); + $atom->setSelfLink($self); + + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + + break; + case 'json': + $this->showJsonTimeline($this->notices); + break; + case 'as': + header('Content-Type: ' . ActivityStreamJSONDocument::CONTENT_TYPE); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + $this->raw($doc->asString()); + break; + default: + // TRANS: Client error displayed when coming across a non-supported API method. + $this->clientError(_('API method not found.'), $code = 404); + break; + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + function lastModified() + { + if (!empty($this->notices) && (count($this->notices) > 0)) { + return strtotime($this->notices[0]->created); + } + + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + function etag() + { + if (!empty($this->notices) && (count($this->notices) > 0)) { + + $last = count($this->notices) - 1; + + return '"' . implode( + ':', + array($this->arg('action'), + common_user_cache_hash($this->auth_user), + common_language(), + $this->user->id, + strtotime($this->notices[0]->created), + strtotime($this->notices[$last]->created)) + ) + . '"'; + } + + return null; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return false; + } else { + return true; + } + } +} diff --git a/api-changes-1.1.1/actions/apiexternalprofileshow.php b/api-changes-1.1.1/actions/apiexternalprofileshow.php new file mode 100644 index 0000000..7d33624 --- /dev/null +++ b/api-changes-1.1.1/actions/apiexternalprofileshow.php @@ -0,0 +1,99 @@ +. + * + * @category API + * @package StatusNet + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiprivateauth.php'; + +/** + * Ouputs information for a user, specified by ID or screen name. + * The user's most recent status will be returned inline. + */ +class ApiExternalProfileShowAction extends ApiPrivateAuthAction +{ + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + function prepare($args) + { + parent::prepare($args); + + $profileurl = urldecode($this->arg('profileurl')); + + $this->profile = Profile::staticGet('profileurl', $profileurl); + + return true; + } + + /** + * Handle the request + * + * Check the format and show the user info + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if (empty($this->profile)) { + // TRANS: Client error displayed when requesting profile information for a non-existing profile. + $this->clientError(_('Profile not found.'), 404, 'json'); + return; + } + + $twitter_user = $this->twitterUserArray($this->profile, true); + + $this->initDocument('json'); + $this->showJsonObjects($twitter_user); + $this->endDocument('json'); + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return true; + } +} diff --git a/api-changes-1.1.1/actions/apisearchjson.php b/api-changes-1.1.1/actions/apisearchjson.php new file mode 100644 index 0000000..805cc0a --- /dev/null +++ b/api-changes-1.1.1/actions/apisearchjson.php @@ -0,0 +1,143 @@ +. + * + * @category Search + * @package StatusNet + * @author Zach Copley + * @copyright 2008-2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/apiprivateauth.php'; +require_once INSTALLDIR.'/lib/jsonsearchresultslist.php'; + +/** + * Action handler for Twitter-compatible API search + * + * @category Search + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * @see ApiAction + */ +class ApiSearchJSONAction extends ApiPrivateAuthAction +{ + var $query; + var $lang; + var $rpp; + var $page; + var $since_id; + var $limit; + var $geocode; + + /** + * Initialization. + * + * @param array $args Web and URL arguments + * + * @return boolean true if nothing goes wrong + */ + function prepare($args) + { + parent::prepare($args); + + $this->query = $this->trimmed('q'); + $this->lang = $this->trimmed('lang'); + $this->rpp = $this->trimmed('rpp'); + + if (!$this->rpp) { + $this->rpp = 15; + } + + if ($this->rpp > 100) { + $this->rpp = 100; + } + + $this->page = $this->trimmed('page'); + + if (!$this->page) { + $this->page = 1; + } + + // TODO: Suppport max_id -- we need to tweak the backend + // Search classes to support it. + + $this->since_id = $this->trimmed('since_id'); + $this->geocode = $this->trimmed('geocode'); + + return true; + } + + /** + * Handle a request + * + * @param array $args Arguments from $_REQUEST + * + * @return void + */ + function handle($args) + { + parent::handle($args); + $this->showResults(); + } + + /** + * Show search results + * + * @return void + */ + function showResults() + { + // TODO: Support search operators like from: and to:, boolean, etc. + + $notice = new Notice(); + + // lcase it for comparison + $q = strtolower($this->query); + + $this->notices = array(); + $search_engine = $notice->getSearchEngine('notice'); + $search_engine->set_sort_mode('chron'); + $search_engine->limit(($this->page - 1) * $this->rpp, $this->rpp + 1); + if ($search_engine->query($q)) { + $cnt = $notice->find(); + $this->notices = $notice->fetchAll(); + } + + $this->showJsonTimeline($this->notices); + } + + /** + * Do we need to write to the database? + * + * @return boolean true + */ + function isReadOnly($args) + { + return true; + } +} diff --git a/api-changes-1.1.1/actions/apistatusesfavs.php b/api-changes-1.1.1/actions/apistatusesfavs.php new file mode 100644 index 0000000..e5b8dc3 --- /dev/null +++ b/api-changes-1.1.1/actions/apistatusesfavs.php @@ -0,0 +1,139 @@ +. + * + * @category API + * @package StatusNet + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; +require_once INSTALLDIR . '/lib/mediafile.php'; + +/** + * Show up to 100 favs of a notice + * + */ +class ApiStatusesFavsAction extends ApiAuthAction +{ + const MAXCOUNT = 100; + + var $original = null; + var $cnt = self::MAXCOUNT; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $id = $this->trimmed('id'); + + $this->original = Notice::staticGet('id', $id); + + if (empty($this->original)) { + // TRANS: Client error displayed trying to display redents of a non-exiting notice. + $this->clientError(_('No such notice.'), + 400, $this->format); + return false; + } + + $cnt = $this->trimmed('count'); + + if (empty($cnt) || !is_integer($cnt)) { + $cnt = 100; + } else { + $this->cnt = min((int)$cnt, self::MAXCOUNT); + } + + return true; + } + + /** + * Handle the request + * + * Get favs and return them as json object + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + $fave = new Fave(); + $fave->selectAdd(); + $fave->selectAdd('user_id'); + $fave->notice_id = $this->original->id; + $fave->orderBy('modified'); + if (!is_null($this->cnt)) { + $fave->limit(0, $this->cnt); + } + + $ids = $fave->fetchAll('user_id'); + + // get nickname and profile image + $ids_with_profile_data = array(); + $i=0; + foreach($ids as $id) { + $profile = Profile::staticGet('id', $id); + $ids_with_profile_data[$i]['user_id'] = $id; + $ids_with_profile_data[$i]['nickname'] = $profile->nickname; + $ids_with_profile_data[$i]['fullname'] = $profile->fullname; + $ids_with_profile_data[$i]['profileurl'] = $profile->profileurl; + $profile = new Profile(); + $profile->id = $id; + $avatarurl = $profile->avatarUrl(24); + $ids_with_profile_data[$i]['avatarurl'] = $avatarurl; + $i++; + } + + $this->initDocument('json'); + $this->showJsonObjects($ids_with_profile_data); + $this->endDocument('json'); + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return true; + } +} diff --git a/api-changes-1.1.1/actions/apistatusesupdate.php b/api-changes-1.1.1/actions/apistatusesupdate.php new file mode 100644 index 0000000..3c48393 --- /dev/null +++ b/api-changes-1.1.1/actions/apistatusesupdate.php @@ -0,0 +1,380 @@ +. + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author Tom Blankenship + * @author Mike Cochrane + * @author Robin Millette + * @author Zach Copley + * @copyright 2009-2010 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +/* External API usage documentation. Please update when you change how this method works. */ + +/*! @page statusesupdate statuses/update + + @section Description + Updates the authenticating user's status. Requires the status parameter specified below. + Request must be a POST. + + @par URL pattern + /api/statuses/update.:format + + @par Formats (:format) + xml, json + + @par HTTP Method(s) + POST + + @par Requires Authentication + Yes + + @param status (Required) The URL-encoded text of the status update. + @param source (Optional) The source application name, if using HTTP authentication or an anonymous OAuth consumer. + @param in_reply_to_status_id (Optional) The ID of an existing status that the update is in reply to. + @param lat (Optional) The latitude the status refers to. + @param long (Optional) The longitude the status refers to. + @param media (Optional) a media upload, such as an image or movie file. + + @sa @ref authentication + @sa @ref apiroot + + @subsection usagenotes Usage notes + + @li The URL pattern is relative to the @ref apiroot. + @li If the @e source parameter is not supplied the source of the status will default to 'api'. When authenticated via a registered OAuth application, the application's registered name and URL will always override the source parameter. + @li The XML response uses GeoRSS + to encode the latitude and longitude (see example response below ). + @li Data uploaded via the @e media parameter should be multipart/form-data encoded. + + @subsection exampleusage Example usage + + @verbatim + curl -u username:password http://example.com/api/statuses/update.xml -d status='Howdy!' -d lat='30.468' -d long='-94.743' + @endverbatim + + @subsection exampleresponse Example response + + @verbatim + + + Howdy! + false + Tue Mar 30 23:28:05 +0000 2010 + + api + 26668724 + + + + 30.468 -94.743 + + false + + 25803 + Jed Sanders + jedsanders + Hoop and Holler, Texas + I like to think of myself as America's Favorite. + http://avatar.example.com/25803-48-20080924200604.png + http://jedsanders.net + false + 5 + + + + + + 2 + Wed Sep 24 20:04:00 +0000 2008 + 0 + 0 + UTC + + false + 70 + true + true + + + @endverbatim +*/ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; +require_once INSTALLDIR . '/lib/mediafile.php'; + +/** + * Updates the authenticating user's status (posts a notice). + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author Tom Blankenship + * @author Mike Cochrane + * @author Robin Millette + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class ApiStatusesUpdateAction extends ApiAuthAction +{ + var $status = null; + var $in_reply_to_status_id = null; + var $lat = null; + var $lon = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->status = $this->trimmed('status'); + $this->lat = $this->trimmed('lat'); + $this->lon = $this->trimmed('long'); + + $this->in_reply_to_status_id + = intval($this->trimmed('in_reply_to_status_id')); + + return true; + } + + /** + * Handle the request + * + * Make a new notice for the update, save it, and show it + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError( + // TRANS: Client error. POST is a HTTP command. It should not be translated. + _('This method requires a POST.'), + 400, + $this->format + ); + return; + } + + // Workaround for PHP returning empty $_POST and $_FILES when POST + // length > post_max_size in php.ini + + if (empty($_FILES) + && empty($_POST) + && ($_SERVER['CONTENT_LENGTH'] > 0) + ) { + // TRANS: Client error displayed when the number of bytes in a POST request exceeds a limit. + // TRANS: %s is the number of bytes of the CONTENT_LENGTH. + $msg = _m('The server was unable to handle that much POST data (%s byte) due to its current configuration.', + 'The server was unable to handle that much POST data (%s bytes) due to its current configuration.', + intval($_SERVER['CONTENT_LENGTH'])); + + $this->clientError(sprintf($msg, $_SERVER['CONTENT_LENGTH'])); + return; + } + + if (empty($this->status)) { + $this->clientError( + // TRANS: Client error displayed when the parameter "status" is missing. + _('Client must provide a \'status\' parameter with a value.'), + 400, + $this->format + ); + return; + } + + if (empty($this->auth_user)) { + // TRANS: Client error displayed when updating a status for a non-existing user. + $this->clientError(_('No such user.'), 404, $this->format); + return; + } + + /* Do not call shortenlinks until the whole notice has been build */ + + // Check for commands + + $inter = new CommandInterpreter(); + $cmd = $inter->handle_command($this->auth_user, $this->status); + + if ($cmd) { + if ($this->supported($cmd)) { + $cmd->execute(new Channel()); + } + + // Cmd not supported? Twitter just returns your latest status. + // And, it returns your last status whether the cmd was successful + // or not! + + $this->notice = $this->auth_user->getCurrentNotice(); + } else { + $reply_to = null; + + if (!empty($this->in_reply_to_status_id)) { + // Check whether notice actually exists + + $reply = Notice::staticGet($this->in_reply_to_status_id); + + if ($reply) { + $reply_to = $this->in_reply_to_status_id; + } else { + $this->clientError( + // TRANS: Client error displayed when replying to a non-existing notice. + _('Parent notice not found.'), + $code = 404, + $this->format + ); + return; + } + } + + $upload = null; + + try { + $upload = MediaFile::fromUpload('media', $this->auth_user); + } catch (Exception $e) { + $this->clientError($e->getMessage(), $e->getCode(), $this->format); + return; + } + + if (isset($upload)) { + $this->status .= ' ' . $upload->shortUrl(); + + /* Do not call shortenlinks until the whole notice has been build */ + } + + /* Do call shortenlinks here & check notice length since notice is about to be saved & sent */ + //$status_shortened = $this->auth_user->shortenlinks($this->status); + + /* DO NOT! */ + $status_shortened = $this->status; + + if (Notice::contentTooLong($status_shortened)) { + if (isset($upload)) { + $upload->delete(); + } + // TRANS: Client error displayed exceeding the maximum notice length. + // TRANS: %d is the maximum lenth for a notice. + $msg = _m('Maximum notice size is %d character, including attachment URL.', + 'Maximum notice size is %d characters, including attachment URL.', + Notice::maxContent()); + /* Use HTTP 413 error code (Request Entity Too Large) + * instead of basic 400 for better understanding + */ + $this->clientError(sprintf($msg, Notice::maxContent()), + 413, + $this->format); + } + + + $content = html_entity_decode($status_shortened, ENT_NOQUOTES, 'UTF-8'); + + $options = array('reply_to' => $reply_to); + + if ($this->auth_user->shareLocation()) { + + $locOptions = Notice::locationOptions($this->lat, + $this->lon, + null, + null, + $this->auth_user->getProfile()); + + $options = array_merge($options, $locOptions); + } + + try { + $this->notice = Notice::saveNew( + $this->auth_user->id, + $content, + $this->source, + $options + ); + } catch (Exception $e) { + $this->clientError($e->getMessage(), $e->getCode(), $this->format); + return; + } + + if (isset($upload)) { + $upload->attachToNotice($this->notice); + } + } + + $this->showNotice(); + } + + /** + * Show the resulting notice + * + * @return void + */ + function showNotice() + { + if (!empty($this->notice)) { + if ($this->format == 'xml') { + $this->showSingleXmlStatus($this->notice); + } elseif ($this->format == 'json') { + $this->show_single_json_status($this->notice); + } + } + } + + /** + * Is this command supported when doing an update from the API? + * + * @param string $cmd the command to check for + * + * @return boolean true or false + */ + function supported($cmd) + { + static $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand', + 'FavCommand', 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand'); + + if (in_array(get_class($cmd), $cmdlist)) { + return true; + } + + return false; + } +} diff --git a/api-changes-1.1.1/actions/apitimelinetag.php b/api-changes-1.1.1/actions/apitimelinetag.php new file mode 100644 index 0000000..4b5a675 --- /dev/null +++ b/api-changes-1.1.1/actions/apitimelinetag.php @@ -0,0 +1,248 @@ +. + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author Zach Copley + * @copyright 2009-2010 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiprivateauth.php'; + +/** + * Returns the 20 most recent notices tagged by a given tag + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class ApiTimelineTagAction extends ApiPrivateAuthAction +{ + var $notices = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + common_debug("apitimelinetag prepare()"); + + $this->tag = $this->arg('tag'); + $this->notices = $this->getNotices(); + + return true; + } + + /** + * Handle the request + * + * Just show the notices + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + $this->showTimeline(); + } + + /** + * Show the timeline of notices + * + * @return void + */ + function showTimeline() + { + $sitename = common_config('site', 'name'); + $sitelogo = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'); + // TRANS: Title for timeline with lastest notices with a given tag. + // TRANS: %s is the tag. + $title = sprintf(_("Notices tagged with %s"), $this->tag); + $subtitle = sprintf( + // TRANS: Subtitle for timeline with lastest notices with a given tag. + // TRANS: %1$s is the tag, $2$s is the StatusNet sitename. + _('Updates tagged with %1$s on %2$s!'), + $this->tag, + $sitename + ); + $taguribase = TagURI::base(); + $id = "tag:$taguribase:TagTimeline:".$this->tag; + + $link = common_local_url( + 'tag', + array('tag' => $this->tag) + ); + + $self = $this->getSelfUri(); + + switch($this->format) { + case 'xml': + $this->showXmlTimeline($this->notices); + break; + case 'rss': + $this->showRssTimeline( + $this->notices, + $title, + $link, + $subtitle, + null, + $sitelogo, + $self + ); + break; + case 'atom': + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed($this->auth_user); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setSubtitle($subtitle); + $atom->setLogo($logo); + $atom->setUpdated('now'); + + $atom->addLink($link); + $atom->setSelfLink($self); + + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + + break; + case 'json': + $this->showJsonTimeline($this->notices); + break; + case 'as': + header('Content-Type: ' . ActivityStreamJSONDocument::CONTENT_TYPE); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + $this->raw($doc->asString()); + break; + default: + // TRANS: Client error displayed when coming across a non-supported API method. + $this->clientError(_('API method not found.'), $code = 404); + break; + } + } + + /** + * Get notices + * + * @return array notices + */ + function getNotices() + { + $notices = array(); + + $notice = Notice_tag::getStream( + $this->tag, + ($this->page - 1) * $this->count, + $this->count + 1, + $this->since_id, + $this->max_id + ); + + while ($notice->fetch()) { + $notices[] = clone($notice); + } + + return $notices; + } + + /** + * Is this action read only? + * + * @param array $args other arguments + * + * @return boolean true + */ + function isReadOnly($args) + { + return true; + } + + /** + * When was this feed last modified? + * + * @return string datestamp of the latest notice in the stream + */ + function lastModified() + { + if (!empty($this->notices) && (count($this->notices) > 0)) { + return strtotime($this->notices[0]->created); + } + + return null; + } + + /** + * An entity tag for this stream + * + * Returns an Etag based on the action name, language, and + * timestamps of the first and last notice in the timeline + * + * @return string etag + */ + function etag() + { + if (!empty($this->notices) && (count($this->notices) > 0)) { + + $last = count($this->notices) - 1; + + return '"' . implode( + ':', + array($this->arg('action'), + common_user_cache_hash($this->auth_user), + common_language(), + $this->tag, + strtotime($this->notices[0]->created), + strtotime($this->notices[$last]->created)) + ) + . '"'; + } + + return null; + } +} diff --git a/api-changes-1.1.1/actions/apitimelineuser.php b/api-changes-1.1.1/actions/apitimelineuser.php new file mode 100644 index 0000000..9b8fa6d --- /dev/null +++ b/api-changes-1.1.1/actions/apitimelineuser.php @@ -0,0 +1,524 @@ +. + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author mac65 + * @author Mike Cochrane + * @author Robin Millette + * @author Zach Copley + * @copyright 2009 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apibareauth.php'; + +/** + * Returns the most recent notices (default 20) posted by the authenticating + * user. Another user's timeline can be requested via the id parameter. This + * is the API equivalent of the user profile web page. + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author mac65 + * @author Mike Cochrane + * @author Robin Millette + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class ApiTimelineUserAction extends ApiBareAuthAction +{ + var $notices = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->user = $this->getTargetUser($this->arg('id')); + + if (empty($this->user)) { + // TRANS: Client error displayed requesting most recent notices for a non-existing user. + $this->clientError(_('No such user.'), 404, $this->format); + return; + } + + $this->notices = $this->getNotices(); + + return true; + } + + /** + * Handle the request + * + * Just show the notices + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if ($this->isPost()) { + $this->handlePost(); + } else { + $this->showTimeline(); + } + } + + /** + * Show the timeline of notices + * + * @return void + */ + function showTimeline() + { + $profile = $this->user->getProfile(); + + // We'll use the shared params from the Atom stub + // for other feed types. + $atom = new AtomUserNoticeFeed($this->user, $this->auth_user); + + $link = common_local_url( + 'showstream', + array('nickname' => $this->user->nickname) + ); + + $self = $this->getSelfUri(); + + // FriendFeed's SUP protocol + // Also added RSS and Atom feeds + + $suplink = common_local_url('sup', null, null, $this->user->id); + header('X-SUP-ID: ' . $suplink); + + switch($this->format) { + case 'xml': + $this->showXmlTimeline($this->notices); + break; + case 'rss': + $this->showRssTimeline( + $this->notices, + $atom->title, + $link, + $atom->subtitle, + $suplink, + $atom->logo, + $self + ); + break; + case 'atom': + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom->setId($self); + $atom->setSelfLink($self); + + // Add navigation links: next, prev, first + // Note: we use IDs rather than pages for navigation; page boundaries + // change too quickly! + + if (!empty($this->next_id)) { + $nextUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id), + array('max_id' => $this->next_id)); + + $atom->addLink($nextUrl, + array('rel' => 'next', + 'type' => 'application/atom+xml')); + } + + if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) { + + $lastNotice = $this->notices[0]; + $lastId = $lastNotice->id; + + $prevUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id), + array('since_id' => $lastId)); + + $atom->addLink($prevUrl, + array('rel' => 'prev', + 'type' => 'application/atom+xml')); + } + + if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) { + + $firstUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id)); + + $atom->addLink($firstUrl, + array('rel' => 'first', + 'type' => 'application/atom+xml')); + + } + + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + + break; + case 'json': + $this->showJsonTimeline($this->notices); + break; + case 'as': + header('Content-Type: ' . ActivityStreamJSONDocument::CONTENT_TYPE); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($atom->title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + + // XXX: Add paging extension? + + $this->raw($doc->asString()); + break; + default: + // TRANS: Client error displayed when coming across a non-supported API method. + $this->clientError(_('API method not found.'), $code = 404); + break; + } + } + + /** + * Get notices + * + * @return array notices + */ + function getNotices() + { + $notices = array(); + + $user_profile = $this->user->getProfile(); + if(isset($this->auth_user)) { + $auth_user_profile = $this->auth_user->getProfile(); + } + else { + $auth_user_profile = null; + } + + $stream = new ProfileNoticeStream($user_profile, $auth_user_profile); + + $notice = $stream->getNotices(($this->page-1) * $this->count, + $this->count + 1, + $this->since_id, + $this->max_id); + + while ($notice->fetch()) { + if (count($notices) < $this->count) { + $notices[] = clone($notice); + } else { + $this->next_id = $notice->id; + break; + } + } + + return $notices; + } + + /** + * We expose AtomPub here, so non-GET/HEAD reqs must be read/write. + * + * @param array $args other arguments + * + * @return boolean true + */ + + function isReadOnly($args) + { + return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD'); + } + + /** + * When was this feed last modified? + * + * @return string datestamp of the latest notice in the stream + */ + function lastModified() + { + if (!empty($this->notices) && (count($this->notices) > 0)) { + return strtotime($this->notices[0]->created); + } + + return null; + } + + /** + * An entity tag for this stream + * + * Returns an Etag based on the action name, language, user ID, and + * timestamps of the first and last notice in the timeline + * + * @return string etag + */ + function etag() + { + if (!empty($this->notices) && (count($this->notices) > 0)) { + $last = count($this->notices) - 1; + + return '"' . implode( + ':', + array($this->arg('action'), + common_user_cache_hash($this->auth_user), + common_language(), + $this->user->id, + strtotime($this->notices[0]->created), + strtotime($this->notices[$last]->created)) + ) + . '"'; + } + + return null; + } + + function handlePost() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->user->id) { + // TRANS: Client error displayed trying to add a notice to another user's timeline. + $this->clientError(_('Only the user can add to their own timeline.')); + return; + } + + // Only handle posts for Atom + if ($this->format != 'atom') { + // TRANS: Client error displayed when using another format than AtomPub. + $this->clientError(_('Only accept AtomPub for Atom feeds.')); + return; + } + + $xml = trim(file_get_contents('php://input')); + if (empty($xml)) { + // TRANS: Client error displayed attempting to post an empty API notice. + $this->clientError(_('Atom post must not be empty.')); + } + + $old = error_reporting(error_reporting() & ~(E_WARNING | E_NOTICE)); + $dom = new DOMDocument(); + $ok = $dom->loadXML($xml); + error_reporting($old); + if (!$ok) { + // TRANS: Client error displayed attempting to post an API that is not well-formed XML. + $this->clientError(_('Atom post must be well-formed XML.')); + } + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + // TRANS: Client error displayed when not using an Atom entry. + $this->clientError(_('Atom post must be an Atom entry.')); + return; + } + + $activity = new Activity($dom->documentElement); + + $saved = null; + + if (Event::handle('StartAtomPubNewActivity', array(&$activity, $this->user, &$saved))) { + if ($activity->verb != ActivityVerb::POST) { + // TRANS: Client error displayed when not using the POST verb. Do not translate POST. + $this->clientError(_('Can only handle POST activities.')); + return; + } + + $note = $activity->objects[0]; + + if (!in_array($note->type, array(ActivityObject::NOTE, + ActivityObject::BLOGENTRY, + ActivityObject::STATUS))) { + // TRANS: Client error displayed when using an unsupported activity object type. + // TRANS: %s is the unsupported activity object type. + $this->clientError(sprintf(_('Cannot handle activity object type "%s".'), + $note->type)); + return; + } + + $saved = $this->postNote($activity); + + Event::handle('EndAtomPubNewActivity', array($activity, $this->user, $saved)); + } + + if (!empty($saved)) { + header('HTTP/1.1 201 Created'); + header("Location: " . common_local_url('ApiStatusesShow', array('id' => $saved->id, + 'format' => 'atom'))); + $this->showSingleAtomStatus($saved); + } + } + + function postNote($activity) + { + $note = $activity->objects[0]; + + // Use summary as fallback for content + + if (!empty($note->content)) { + $sourceContent = $note->content; + } else if (!empty($note->summary)) { + $sourceContent = $note->summary; + } else if (!empty($note->title)) { + $sourceContent = $note->title; + } else { + // @fixme fetch from $sourceUrl? + // TRANS: Client error displayed when posting a notice without content through the API. + // TRANS: %d is the notice ID (number). + $this->clientError(sprintf(_('No content for notice %d.'), + $note->id)); + return; + } + + // Get (safe!) HTML and text versions of the content + + $rendered = $this->purify($sourceContent); + $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); + + $shortened = $this->auth_user->shortenLinks($content); + + $options = array('is_local' => Notice::LOCAL_PUBLIC, + 'rendered' => $rendered, + 'replies' => array(), + 'groups' => array(), + 'tags' => array(), + 'urls' => array()); + + // accept remote URI (not necessarily a good idea) + + common_debug("Note ID is {$note->id}"); + + if (!empty($note->id)) { + $notice = Notice::staticGet('uri', trim($note->id)); + + if (!empty($notice)) { + // TRANS: Client error displayed when using another format than AtomPub. + // TRANS: %s is the notice URI. + $this->clientError(sprintf(_('Notice with URI "%s" already exists.'), + $note->id)); + return; + } + common_log(LOG_NOTICE, "Saving client-supplied notice URI '$note->id'"); + $options['uri'] = $note->id; + } + + // accept remote create time (also maybe not such a good idea) + + if (!empty($activity->time)) { + common_log(LOG_NOTICE, "Saving client-supplied create time {$activity->time}"); + $options['created'] = common_sql_date($activity->time); + } + + // Check for optional attributes... + + if (!empty($activity->context)) { + + foreach ($activity->context->attention as $uri) { + + $profile = Profile::fromURI($uri); + + if (!empty($profile)) { + $options['replies'][] = $uri; + } else { + $group = User_group::staticGet('uri', $uri); + if (!empty($group)) { + $options['groups'][] = $group->id; + } else { + // @fixme: hook for discovery here + common_log(LOG_WARNING, sprintf('AtomPub post with unknown attention URI %s', $uri)); + } + } + } + + // Maintain direct reply associations + // @fixme what about conversation ID? + + if (!empty($activity->context->replyToID)) { + $orig = Notice::staticGet('uri', + $activity->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; + } + } + + $location = $activity->context->location; + + if ($location) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; + } + } + } + + // Atom categories <-> hashtags + + foreach ($activity->categories as $cat) { + if ($cat->term) { + $term = common_canonical_tag($cat->term); + if ($term) { + $options['tags'][] = $term; + } + } + } + + // Atom enclosures -> attachment URLs + foreach ($activity->enclosures as $href) { + // @fixme save these locally or....? + $options['urls'][] = $href; + } + + $saved = Notice::saveNew($this->user->id, + $content, + 'atompub', // TODO: deal with this + $options); + + return $saved; + } + + function purify($content) + { + require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php'; + + $config = array('safe' => 1, + 'deny_attribute' => 'id,style,on*'); + return htmLawed($content, $config); + } +} diff --git a/api-changes-1.1.1/classes/Notice.php b/api-changes-1.1.1/classes/Notice.php new file mode 100644 index 0000000..9f33ad9 --- /dev/null +++ b/api-changes-1.1.1/classes/Notice.php @@ -0,0 +1,2761 @@ +. + * + * @category Notices + * @package StatusNet + * @author Brenda Wallace + * @author Christopher Vollick + * @author CiaranG + * @author Craig Andrews + * @author Evan Prodromou + * @author Gina Haeussge + * @author Jeffery To + * @author Mike Cochrane + * @author Robin Millette + * @author Sarven Capadisli + * @author Tom Adams + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license GNU Affero General Public License http://www.gnu.org/licenses/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Table Definition for notice + */ +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; + +/* We keep 200 notices, the max number of notices available per API request, + * in the memcached cache. */ + +define('NOTICE_CACHE_WINDOW', CachingNoticeStream::CACHE_WINDOW); + +define('MAX_BOXCARS', 128); + +class Notice extends Managed_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'notice'; // table name + public $id; // int(4) primary_key not_null + public $profile_id; // int(4) multiple_key not_null + public $uri; // varchar(255) unique_key + public $content; // text + public $rendered; // text + public $url; // varchar(255) + public $created; // datetime multiple_key not_null default_0000-00-00%2000%3A00%3A00 + public $modified; // timestamp not_null default_CURRENT_TIMESTAMP + public $reply_to; // int(4) + public $is_local; // int(4) + public $source; // varchar(32) + public $conversation; // int(4) + public $lat; // decimal(10,7) + public $lon; // decimal(10,7) + public $location_id; // int(4) + public $location_ns; // int(4) + public $repeat_of; // int(4) + public $verb; // varchar(255) + public $object_type; // varchar(255) + public $scope; // int(4) + + /* Static get */ + function staticGet($k,$v=NULL) + { + return Memcached_DataObject::staticGet('Notice',$k,$v); + } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE + + public static function schemaDef() + { + $def = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), + 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'who made the update'), + 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'), + 'content' => array('type' => 'text', 'description' => 'update content', 'collate' => 'utf8_general_ci'), + 'rendered' => array('type' => 'text', 'description' => 'HTML version of the content'), + 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + 'reply_to' => array('type' => 'int', 'description' => 'notice replied to (usually a guess)'), + 'is_local' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'notice was generated by a user'), + 'source' => array('type' => 'varchar', 'length' => 32, 'description' => 'source of comment, like "web", "im", or "clientname"'), + 'conversation' => array('type' => 'int', 'description' => 'id of root notice in this conversation'), + 'lat' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'), + 'lon' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'), + 'location_id' => array('type' => 'int', 'description' => 'location id if possible'), + 'location_ns' => array('type' => 'int', 'description' => 'namespace for location'), + 'repeat_of' => array('type' => 'int', 'description' => 'notice this is a repeat of'), + 'object_type' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'), + 'verb' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'), + 'scope' => array('type' => 'int', + 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers; null = default'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'notice_uri_key' => array('uri'), + ), + 'foreign keys' => array( + 'notice_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + 'notice_reply_to_fkey' => array('notice', array('reply_to' => 'id')), + 'notice_conversation_fkey' => array('conversation', array('conversation' => 'id')), # note... used to refer to notice.id + 'notice_repeat_of_fkey' => array('notice', array('repeat_of' => 'id')), # @fixme: what about repeats of deleted notices? + ), + 'indexes' => array( + 'notice_created_id_is_local_idx' => array('created', 'id', 'is_local'), + 'notice_profile_id_idx' => array('profile_id', 'created', 'id'), + 'notice_repeat_of_created_id_idx' => array('repeat_of', 'created', 'id'), + 'notice_conversation_created_id_idx' => array('conversation', 'created', 'id'), + 'notice_replyto_idx' => array('reply_to') + ) + ); + + if (common_config('search', 'type') == 'fulltext') { + $def['fulltext indexes'] = array('content' => array('content')); + } + + return $def; + } + + function multiGet($kc, $kvs, $skipNulls=true) + { + return Memcached_DataObject::multiGet('Notice', $kc, $kvs, $skipNulls); + } + + /* Notice types */ + const LOCAL_PUBLIC = 1; + const REMOTE = 0; + const LOCAL_NONPUBLIC = -1; + const GATEWAY = -2; + + const PUBLIC_SCOPE = 0; // Useful fake constant + const SITE_SCOPE = 1; + const ADDRESSEE_SCOPE = 2; + const GROUP_SCOPE = 4; + const FOLLOWER_SCOPE = 8; + + protected $_profile = -1; + + function getProfile() + { + if (is_int($this->_profile) && $this->_profile == -1) { + $this->_setProfile(Profile::staticGet('id', $this->profile_id)); + + if (empty($this->_profile)) { + // TRANS: Server exception thrown when a user profile for a notice cannot be found. + // TRANS: %1$d is a profile ID (number), %2$d is a notice ID (number). + throw new ServerException(sprintf(_('No such profile (%1$d) for notice (%2$d).'), $this->profile_id, $this->id)); + } + } + + return $this->_profile; + } + + function _setProfile($profile) + { + $this->_profile = $profile; + } + + function delete() + { + // For auditing purposes, save a record that the notice + // was deleted. + + // @fixme we have some cases where things get re-run and so the + // insert fails. + $deleted = Deleted_notice::staticGet('id', $this->id); + + if (!$deleted) { + $deleted = Deleted_notice::staticGet('uri', $this->uri); + } + + if (!$deleted) { + $deleted = new Deleted_notice(); + + $deleted->id = $this->id; + $deleted->profile_id = $this->profile_id; + $deleted->uri = $this->uri; + $deleted->created = $this->created; + $deleted->deleted = common_sql_now(); + + $deleted->insert(); + } + + if (Event::handle('NoticeDeleteRelated', array($this))) { + + // Clear related records + + $this->clearReplies(); + $this->clearRepeats(); + $this->clearFaves(); + $this->clearTags(); + $this->clearGroupInboxes(); + $this->clearFiles(); + + // NOTE: we don't clear inboxes + // NOTE: we don't clear queue items + } + + $result = parent::delete(); + + $this->blowOnDelete(); + return $result; + } + + /** + * Extract #hashtags from this notice's content and save them to the database. + */ + function saveTags() + { + /* extract all #hastags */ + $count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/u', strtolower($this->content), $match); + if (!$count) { + return true; + } + + /* Add them to the database */ + return $this->saveKnownTags($match[1]); + } + + /** + * Record the given set of hash tags in the db for this notice. + * Given tag strings will be normalized and checked for dupes. + */ + function saveKnownTags($hashtags) + { + //turn each into their canonical tag + //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag + for($i=0; $isaveTag($hashtag); + self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, $hashtag); + } + return true; + } + + /** + * Record a single hash tag as associated with this notice. + * Tag format and uniqueness must be validated by caller. + */ + function saveTag($hashtag) + { + $tag = new Notice_tag(); + $tag->notice_id = $this->id; + $tag->tag = $hashtag; + $tag->created = $this->created; + $id = $tag->insert(); + + if (!$id) { + // TRANS: Server exception. %s are the error details. + throw new ServerException(sprintf(_('Database error inserting hashtag: %s.'), + $last_error->message)); + return; + } + + // if it's saved, blow its cache + $tag->blowCache(false); + } + + /** + * Save a new notice and push it out to subscribers' inboxes. + * Poster's permissions are checked before sending. + * + * @param int $profile_id Profile ID of the poster + * @param string $content source message text; links may be shortened + * per current user's preference + * @param string $source source key ('web', 'api', etc) + * @param array $options Associative array of optional properties: + * string 'created' timestamp of notice; defaults to now + * int 'is_local' source/gateway ID, one of: + * Notice::LOCAL_PUBLIC - Local, ok to appear in public timeline + * Notice::REMOTE - Sent from a remote service; + * hide from public timeline but show in + * local "and friends" timelines + * Notice::LOCAL_NONPUBLIC - Local, but hide from public timeline + * Notice::GATEWAY - From another non-OStatus service; + * will not appear in public views + * float 'lat' decimal latitude for geolocation + * float 'lon' decimal longitude for geolocation + * int 'location_id' geoname identifier + * int 'location_ns' geoname namespace to interpret location_id + * int 'reply_to'; notice ID this is a reply to + * int 'repeat_of'; notice ID this is a repeat of + * string 'uri' unique ID for notice; defaults to local notice URL + * string 'url' permalink to notice; defaults to local notice URL + * string 'rendered' rendered HTML version of content + * array 'replies' list of profile URIs for reply delivery in + * place of extracting @-replies from content. + * array 'groups' list of group IDs to deliver to, in place of + * extracting ! tags from content + * array 'tags' list of hashtag strings to save with the notice + * in place of extracting # tags from content + * array 'urls' list of attached/referred URLs to save with the + * notice in place of extracting links from content + * boolean 'distribute' whether to distribute the notice, default true + * string 'object_type' URL of the associated object type (default ActivityObject::NOTE) + * string 'verb' URL of the associated verb (default ActivityVerb::POST) + * int 'scope' Scope bitmask; default to SITE_SCOPE on private sites, 0 otherwise + * + * @fixme tag override + * + * @return Notice + * @throws ClientException + */ + static function saveNew($profile_id, $content, $source, $options=null) { + $defaults = array('uri' => null, + 'url' => null, + 'reply_to' => null, + 'repeat_of' => null, + 'scope' => null, + 'distribute' => true, + 'object_type' => null, + 'verb' => null); + + if (!empty($options) && is_array($options)) { + $options = array_merge($defaults, $options); + extract($options); + } else { + extract($defaults); + } + + if (!isset($is_local)) { + $is_local = Notice::LOCAL_PUBLIC; + } + + $profile = Profile::staticGet('id', $profile_id); + $user = User::staticGet('id', $profile_id); + +// no shortening, only clientside +// if ($user) { +// // Use the local user's shortening preferences, if applicable. +// $final = $user->shortenLinks($content); +// } else { +// $final = common_shorten_links($content); +// } + + $final=$content; + + if (Notice::contentTooLong($final)) { + // TRANS: Client exception thrown if a notice contains too many characters. + throw new ClientException(_('Problem saving notice. Too long.')); + } + + if (empty($profile)) { + // TRANS: Client exception thrown when trying to save a notice for an unknown user. + throw new ClientException(_('Problem saving notice. Unknown user.')); + } + + if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) { + common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.'); + // TRANS: Client exception thrown when a user tries to post too many notices in a given time frame. + throw new ClientException(_('Too many notices too fast; take a breather '. + 'and post again in a few minutes.')); + } + + if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) { + common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.'); + // TRANS: Client exception thrown when a user tries to post too many duplicate notices in a given time frame. + throw new ClientException(_('Too many duplicate messages too quickly;'. + ' take a breather and post again in a few minutes.')); + } + + if (!$profile->hasRight(Right::NEWNOTICE)) { + common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $profile->nickname); + + // TRANS: Client exception thrown when a user tries to post while being banned. + throw new ClientException(_('You are banned from posting notices on this site.'), 403); + } + + $notice = new Notice(); + $notice->profile_id = $profile_id; + + $autosource = common_config('public', 'autosource'); + + // Sandboxed are non-false, but not 1, either + + if (!$profile->hasRight(Right::PUBLICNOTICE) || + ($source && $autosource && in_array($source, $autosource))) { + $notice->is_local = Notice::LOCAL_NONPUBLIC; + } else { + $notice->is_local = $is_local; + } + + if (!empty($created)) { + $notice->created = $created; + } else { + $notice->created = common_sql_now(); + } + + $notice->content = $final; + + $notice->source = $source; + $notice->uri = $uri; + $notice->url = $url; + + // Get the groups here so we can figure out replies and such + + if (!isset($groups)) { + $groups = self::groupsFromText($notice->content, $profile); + } + + $reply = null; + + // Handle repeat case + + if (isset($repeat_of)) { + + // Check for a private one + + $repeat = Notice::staticGet('id', $repeat_of); + + if (empty($repeat)) { + // TRANS: Client exception thrown in notice when trying to repeat a missing or deleted notice. + throw new ClientException(_('Cannot repeat; original notice is missing or deleted.')); + } + + if ($profile->id == $repeat->profile_id) { + // TRANS: Client error displayed when trying to repeat an own notice. + throw new ClientException(_('You cannot repeat your own notice.')); + } + + if ($repeat->scope != Notice::SITE_SCOPE && + $repeat->scope != Notice::PUBLIC_SCOPE) { + // TRANS: Client error displayed when trying to repeat a non-public notice. + throw new ClientException(_('Cannot repeat a private notice.'), 403); + } + + if (!$repeat->inScope($profile)) { + // The generic checks above should cover this, but let's be sure! + // TRANS: Client error displayed when trying to repeat a notice you cannot access. + throw new ClientException(_('Cannot repeat a notice you cannot read.'), 403); + } + + if ($profile->hasRepeated($repeat->id)) { + // TRANS: Client error displayed when trying to repeat an already repeated notice. + throw new ClientException(_('You already repeated that notice.')); + } + + $notice->repeat_of = $repeat_of; + } else { + $reply = self::getReplyTo($reply_to, $profile_id, $source, $final); + + if (!empty($reply)) { + + if (!$reply->inScope($profile)) { + // TRANS: Client error displayed when trying to reply to a notice a the target has no access to. + // TRANS: %1$s is a user nickname, %2$d is a notice ID (number). + throw new ClientException(sprintf(_('%1$s has no access to notice %2$d.'), + $profile->nickname, $reply->id), 403); + } + + $notice->reply_to = $reply->id; + $notice->conversation = $reply->conversation; + + // If the original is private to a group, and notice has no group specified, + // make it to the same group(s) + + if (empty($groups) && ($reply->scope | Notice::GROUP_SCOPE)) { + $groups = array(); + $replyGroups = $reply->getGroups(); + foreach ($replyGroups as $group) { + if ($profile->isMember($group)) { + $groups[] = $group->id; + } + } + } + + // Scope set below + } + } + + if (!empty($lat) && !empty($lon)) { + $notice->lat = $lat; + $notice->lon = $lon; + } + + if (!empty($location_ns) && !empty($location_id)) { + $notice->location_id = $location_id; + $notice->location_ns = $location_ns; + } + + if (!empty($rendered)) { + $notice->rendered = $rendered; + } else { + $notice->rendered = common_render_content($final, $notice); + } + + if (empty($verb)) { + if (!empty($notice->repeat_of)) { + $notice->verb = ActivityVerb::SHARE; + $notice->object_type = ActivityObject::ACTIVITY; + } else { + $notice->verb = ActivityVerb::POST; + } + } else { + $notice->verb = $verb; + } + + if (empty($object_type)) { + $notice->object_type = (empty($notice->reply_to)) ? ActivityObject::NOTE : ActivityObject::COMMENT; + } else { + $notice->object_type = $object_type; + } + + if (is_null($scope)) { // 0 is a valid value + if (!empty($reply)) { + $notice->scope = $reply->scope; + } else { + $notice->scope = self::defaultScope(); + } + } else { + $notice->scope = $scope; + } + + // For private streams + + $user = $profile->getUser(); + + if (!empty($user)) { + if ($user->private_stream && + ($notice->scope == Notice::PUBLIC_SCOPE || + $notice->scope == Notice::SITE_SCOPE)) { + $notice->scope |= Notice::FOLLOWER_SCOPE; + } + } + + // Force the scope for private groups + + foreach ($groups as $groupId) { + $group = User_group::staticGet('id', $groupId); + if (!empty($group)) { + if ($group->force_scope) { + $notice->scope |= Notice::GROUP_SCOPE; + break; + } + } + } + + if (Event::handle('StartNoticeSave', array(&$notice))) { + + // XXX: some of these functions write to the DB + + $id = $notice->insert(); + + if (!$id) { + common_log_db_error($notice, 'INSERT', __FILE__); + // TRANS: Server exception thrown when a notice cannot be saved. + throw new ServerException(_('Problem saving notice.')); + } + + // Update ID-dependent columns: URI, conversation + + $orig = clone($notice); + + $changed = false; + + if (empty($uri)) { + $notice->uri = common_notice_uri($notice); + $changed = true; + } + + // If it's not part of a conversation, it's + // the beginning of a new conversation. + + if (empty($notice->conversation)) { + $conv = Conversation::create(); + $notice->conversation = $conv->id; + $changed = true; + } + + if ($changed) { + if (!$notice->update($orig)) { + common_log_db_error($notice, 'UPDATE', __FILE__); + // TRANS: Server exception thrown when a notice cannot be updated. + throw new ServerException(_('Problem saving notice.')); + } + } + + } + + // Clear the cache for subscribed users, so they'll update at next request + // XXX: someone clever could prepend instead of clearing the cache + + $notice->blowOnInsert(); + + // Save per-notice metadata... + + if (isset($replies)) { + $notice->saveKnownReplies($replies); + } else { + $notice->saveReplies(); + } + + if (isset($tags)) { + $notice->saveKnownTags($tags); + } else { + $notice->saveTags(); + } + + // Note: groups may save tags, so must be run after tags are saved + // to avoid errors on duplicates. + // Note: groups should always be set. + + $notice->saveKnownGroups($groups); + + if (isset($urls)) { + $notice->saveKnownUrls($urls); + } else { + $notice->saveUrls(); + } + + if ($distribute) { + // Prepare inbox delivery, may be queued to background. + $notice->distribute(); + } + + return $notice; + } + + function blowOnInsert($conversation = false) + { + $this->blowStream('profile:notice_ids:%d', $this->profile_id); + + if ($this->isPublic()) { + $this->blowStream('public'); + } + + self::blow('notice:list-ids:conversation:%s', $this->conversation); + self::blow('conversation:notice_count:%d', $this->conversation); + + if (!empty($this->repeat_of)) { + // XXX: we should probably only use one of these + $this->blowStream('notice:repeats:%d', $this->repeat_of); + self::blow('notice:list-ids:repeat_of:%d', $this->repeat_of); + } + + $original = Notice::staticGet('id', $this->repeat_of); + + if (!empty($original)) { + $originalUser = User::staticGet('id', $original->profile_id); + if (!empty($originalUser)) { + $this->blowStream('user:repeats_of_me:%d', $originalUser->id); + } + } + + $profile = Profile::staticGet($this->profile_id); + + if (!empty($profile)) { + $profile->blowNoticeCount(); + } + + $ptags = $this->getProfileTags(); + foreach ($ptags as $ptag) { + $ptag->blowNoticeStreamCache(); + } + } + + /** + * Clear cache entries related to this notice at delete time. + * Necessary to avoid breaking paging on public, profile timelines. + */ + function blowOnDelete() + { + $this->blowOnInsert(); + + self::blow('profile:notice_ids:%d;last', $this->profile_id); + + if ($this->isPublic()) { + self::blow('public;last'); + } + + self::blow('fave:by_notice', $this->id); + + if ($this->conversation) { + // In case we're the first, will need to calc a new root. + self::blow('notice:conversation_root:%d', $this->conversation); + } + + $ptags = $this->getProfileTags(); + foreach ($ptags as $ptag) { + $ptag->blowNoticeStreamCache(true); + } + } + + function blowStream() + { + $c = self::memcache(); + + if (empty($c)) { + return false; + } + + $args = func_get_args(); + + $format = array_shift($args); + + $keyPart = vsprintf($format, $args); + + $cacheKey = Cache::key($keyPart); + + $c->delete($cacheKey); + + // delete the "last" stream, too, if this notice is + // older than the top of that stream + + $lastKey = $cacheKey.';last'; + + $lastStr = $c->get($lastKey); + + if ($lastStr !== false) { + $window = explode(',', $lastStr); + $lastID = $window[0]; + $lastNotice = Notice::staticGet('id', $lastID); + if (empty($lastNotice) // just weird + || strtotime($lastNotice->created) >= strtotime($this->created)) { + $c->delete($lastKey); + } + } + } + + /** save all urls in the notice to the db + * + * follow redirects and save all available file information + * (mimetype, date, size, oembed, etc.) + * + * @return void + */ + function saveUrls() { + if (common_config('attachments', 'process_links')) { + common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); + } + } + + /** + * Save the given URLs as related links/attachments to the db + * + * follow redirects and save all available file information + * (mimetype, date, size, oembed, etc.) + * + * @return void + */ + function saveKnownUrls($urls) + { + if (common_config('attachments', 'process_links')) { + // @fixme validation? + foreach (array_unique($urls) as $url) { + File::processNew($url, $this->id); + } + } + } + + /** + * @private callback + */ + function saveUrl($url, $notice_id) { + File::processNew($url, $notice_id); + } + + static function checkDupes($profile_id, $content) { + $profile = Profile::staticGet($profile_id); + if (empty($profile)) { + return false; + } + $notice = $profile->getNotices(0, CachingNoticeStream::CACHE_WINDOW); + if (!empty($notice)) { + $last = 0; + while ($notice->fetch()) { + if (time() - strtotime($notice->created) >= common_config('site', 'dupelimit')) { + return true; + } else if ($notice->content == $content) { + return false; + } + } + } + // If we get here, oldest item in cache window is not + // old enough for dupe limit; do direct check against DB + $notice = new Notice(); + $notice->profile_id = $profile_id; + $notice->content = $content; + $threshold = common_sql_date(time() - common_config('site', 'dupelimit')); + $notice->whereAdd(sprintf("created > '%s'", $notice->escape($threshold))); + + $cnt = $notice->count(); + return ($cnt == 0); + } + + static function checkEditThrottle($profile_id) { + $profile = Profile::staticGet($profile_id); + if (empty($profile)) { + return false; + } + // Get the Nth notice + $notice = $profile->getNotices(common_config('throttle', 'count') - 1, 1); + if ($notice && $notice->fetch()) { + // If the Nth notice was posted less than timespan seconds ago + if (time() - strtotime($notice->created) <= common_config('throttle', 'timespan')) { + // Then we throttle + return false; + } + } + // Either not N notices in the stream, OR the Nth was not posted within timespan seconds + return true; + } + + protected $_attachments = -1; + + function attachments() { + + if ($this->_attachments != -1) { + return $this->_attachments; + } + + $f2ps = Memcached_DataObject::listGet('File_to_post', 'post_id', array($this->id)); + + $ids = array(); + + foreach ($f2ps[$this->id] as $f2p) { + $ids[] = $f2p->file_id; + } + + $files = Memcached_DataObject::multiGet('File', 'id', $ids); + + $this->_attachments = $files->fetchAll(); + + return $this->_attachments; + } + + function _setAttachments($attachments) + { + $this->_attachments = $attachments; + } + + function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0) + { + $stream = new PublicNoticeStream(); + return $stream->getNotices($offset, $limit, $since_id, $max_id); + } + + + function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0) + { + $stream = new ConversationNoticeStream($id); + + return $stream->getNotices($offset, $limit, $since_id, $max_id); + } + + /** + * Is this notice part of an active conversation? + * + * @return boolean true if other messages exist in the same + * conversation, false if this is the only one + */ + function hasConversation() + { + if (!empty($this->conversation)) { + $conversation = Notice::conversationStream( + $this->conversation, + 1, + 1 + ); + + if ($conversation->N > 0) { + return true; + } + } + return false; + } + + /** + * Grab the earliest notice from this conversation. + * + * @return Notice or null + */ + function conversationRoot($profile=-1) + { + // XXX: can this happen? + + if (empty($this->conversation)) { + return null; + } + + // Get the current profile if not specified + + if (is_int($profile) && $profile == -1) { + $profile = Profile::current(); + } + + // If this notice is out of scope, no root for you! + + if (!$this->inScope($profile)) { + return null; + } + + // If this isn't a reply to anything, then it's its own + // root. + + if (empty($this->reply_to)) { + return $this; + } + + if (is_null($profile)) { + $keypart = sprintf('notice:conversation_root:%d:null', $this->id); + } else { + $keypart = sprintf('notice:conversation_root:%d:%d', + $this->id, + $profile->id); + } + + $root = self::cacheGet($keypart); + + if ($root !== false && $root->inScope($profile)) { + return $root; + } else { + $last = $this; + + do { + $parent = $last->getOriginal(); + if (!empty($parent) && $parent->inScope($profile)) { + $last = $parent; + continue; + } else { + $root = $last; + break; + } + } while (!empty($parent)); + + self::cacheSet($keypart, $root); + } + + return $root; + } + + /** + * Pull up a full list of local recipients who will be getting + * this notice in their inbox. Results will be cached, so don't + * change the input data wily-nilly! + * + * @param array $groups optional list of Group objects; + * if left empty, will be loaded from group_inbox records + * @param array $recipient optional list of reply profile ids + * if left empty, will be loaded from reply records + * @return array associating recipient user IDs with an inbox source constant + */ + function whoGets($groups=null, $recipients=null) + { + $c = self::memcache(); + + if (!empty($c)) { + $ni = $c->get(Cache::key('notice:who_gets:'.$this->id)); + if ($ni !== false) { + return $ni; + } + } + + if (is_null($groups)) { + $groups = $this->getGroups(); + } + + if (is_null($recipients)) { + $recipients = $this->getReplies(); + } + + $users = $this->getSubscribedUsers(); + $ptags = $this->getProfileTags(); + + // FIXME: kind of ignoring 'transitional'... + // we'll probably stop supporting inboxless mode + // in 0.9.x + + $ni = array(); + + // Give plugins a chance to add folks in at start... + if (Event::handle('StartNoticeWhoGets', array($this, &$ni))) { + + foreach ($users as $id) { + $ni[$id] = NOTICE_INBOX_SOURCE_SUB; + } + + foreach ($groups as $group) { + $users = $group->getUserMembers(); + foreach ($users as $id) { + if (!array_key_exists($id, $ni)) { + $ni[$id] = NOTICE_INBOX_SOURCE_GROUP; + } + } + } + + foreach ($ptags as $ptag) { + $users = $ptag->getUserSubscribers(); + foreach ($users as $id) { + if (!array_key_exists($id, $ni)) { + $ni[$id] = NOTICE_INBOX_SOURCE_PROFILE_TAG; + } + } + } + + foreach ($recipients as $recipient) { + if (!array_key_exists($recipient, $ni)) { + $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY; + } + } + + // Exclude any deleted, non-local, or blocking recipients. + $profile = $this->getProfile(); + $originalProfile = null; + if ($this->repeat_of) { + // Check blocks against the original notice's poster as well. + $original = Notice::staticGet('id', $this->repeat_of); + if ($original) { + $originalProfile = $original->getProfile(); + } + } + + foreach ($ni as $id => $source) { + $user = User::staticGet('id', $id); + if (empty($user) || $user->hasBlocked($profile) || + ($originalProfile && $user->hasBlocked($originalProfile))) { + unset($ni[$id]); + } + } + + // Give plugins a chance to filter out... + Event::handle('EndNoticeWhoGets', array($this, &$ni)); + } + + if (!empty($c)) { + // XXX: pack this data better + $c->set(Cache::key('notice:who_gets:'.$this->id), $ni); + } + + return $ni; + } + + /** + * Adds this notice to the inboxes of each local user who should receive + * it, based on author subscriptions, group memberships, and @-replies. + * + * Warning: running a second time currently will make items appear + * multiple times in users' inboxes. + * + * @fixme make more robust against errors + * @fixme break up massive deliveries to smaller background tasks + * + * @param array $groups optional list of Group objects; + * if left empty, will be loaded from group_inbox records + * @param array $recipient optional list of reply profile ids + * if left empty, will be loaded from reply records + */ + function addToInboxes($groups=null, $recipients=null) + { + $ni = $this->whoGets($groups, $recipients); + + $ids = array_keys($ni); + + // We remove the author (if they're a local user), + // since we'll have already done this in distribute() + + $i = array_search($this->profile_id, $ids); + + if ($i !== false) { + unset($ids[$i]); + } + + // Bulk insert + + Inbox::bulkInsert($this->id, $ids); + + return; + } + + function getSubscribedUsers() + { + $user = new User(); + + if(common_config('db','quote_identifiers')) + $user_table = '"user"'; + else $user_table = 'user'; + + $qry = + 'SELECT id ' . + 'FROM '. $user_table .' JOIN subscription '. + 'ON '. $user_table .'.id = subscription.subscriber ' . + 'WHERE subscription.subscribed = %d '; + + $user->query(sprintf($qry, $this->profile_id)); + + $ids = array(); + + while ($user->fetch()) { + $ids[] = $user->id; + } + + $user->free(); + + return $ids; + } + + function getProfileTags() + { + $profile = $this->getProfile(); + $list = $profile->getOtherTags($profile); + $ptags = array(); + + while($list->fetch()) { + $ptags[] = clone($list); + } + + return $ptags; + } + + /** + * Record this notice to the given group inboxes for delivery. + * Overrides the regular parsing of !group markup. + * + * @param string $group_ids + * @fixme might prefer URIs as identifiers, as for replies? + * best with generalizations on user_group to support + * remote groups better. + */ + function saveKnownGroups($group_ids) + { + if (!is_array($group_ids)) { + // TRANS: Server exception thrown when no array is provided to the method saveKnownGroups(). + throw new ServerException(_('Bad type provided to saveKnownGroups.')); + } + + $groups = array(); + foreach (array_unique($group_ids) as $id) { + $group = User_group::staticGet('id', $id); + if ($group) { + common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname"); + $result = $this->addToGroupInbox($group); + if (!$result) { + common_log_db_error($gi, 'INSERT', __FILE__); + } + + if (common_config('group', 'addtag')) { + // we automatically add a tag for every group name, too + + $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($group->nickname), + 'notice_id' => $this->id)); + + if (is_null($tag)) { + $this->saveTag($group->nickname); + } + } + + $groups[] = clone($group); + } else { + common_log(LOG_ERR, "Local delivery to group id $id skipped, doesn't exist"); + } + } + + return $groups; + } + + /** + * Parse !group delivery and record targets into group_inbox. + * @return array of Group objects + */ + function saveGroups() + { + // Don't save groups for repeats + + if (!empty($this->repeat_of)) { + return array(); + } + + $profile = $this->getProfile(); + + $groups = self::groupsFromText($this->content, $profile); + + /* Add them to the database */ + + foreach ($groups as $group) { + /* XXX: remote groups. */ + + if (empty($group)) { + continue; + } + + + if ($profile->isMember($group)) { + + $result = $this->addToGroupInbox($group); + + if (!$result) { + common_log_db_error($gi, 'INSERT', __FILE__); + } + + $groups[] = clone($group); + } + } + + return $groups; + } + + function addToGroupInbox($group) + { + $gi = Group_inbox::pkeyGet(array('group_id' => $group->id, + 'notice_id' => $this->id)); + + if (empty($gi)) { + + $gi = new Group_inbox(); + + $gi->group_id = $group->id; + $gi->notice_id = $this->id; + $gi->created = $this->created; + + $result = $gi->insert(); + + if (!$result) { + common_log_db_error($gi, 'INSERT', __FILE__); + // TRANS: Server exception thrown when an update for a group inbox fails. + throw new ServerException(_('Problem saving group inbox.')); + } + + self::blow('user_group:notice_ids:%d', $gi->group_id); + } + + return true; + } + + /** + * Save reply records indicating that this notice needs to be + * delivered to the local users with the given URIs. + * + * Since this is expected to be used when saving foreign-sourced + * messages, we won't deliver to any remote targets as that's the + * source service's responsibility. + * + * Mail notifications etc will be handled later. + * + * @param array of unique identifier URIs for recipients + */ + function saveKnownReplies($uris) + { + if (empty($uris)) { + return; + } + + $sender = Profile::staticGet($this->profile_id); + + foreach (array_unique($uris) as $uri) { + + $profile = Profile::fromURI($uri); + + if (empty($profile)) { + common_log(LOG_WARNING, "Unable to determine profile for URI '$uri'"); + continue; + } + + if ($profile->hasBlocked($sender)) { + common_log(LOG_INFO, "Not saving reply to profile {$profile->id} ($uri) from sender {$sender->id} because of a block."); + continue; + } + + $this->saveReply($profile->id); + self::blow('reply:stream:%d', $profile->id); + } + + return; + } + + /** + * Pull @-replies from this message's content in StatusNet markup format + * and save reply records indicating that this message needs to be + * delivered to those users. + * + * Mail notifications to local profiles will be sent later. + * + * @return array of integer profile IDs + */ + + function saveReplies() + { + // Don't save reply data for repeats + + if (!empty($this->repeat_of)) { + return array(); + } + + $sender = Profile::staticGet($this->profile_id); + + $replied = array(); + + // If it's a reply, save for the replied-to author + + if (!empty($this->reply_to)) { + $original = $this->getOriginal(); + if (!empty($original)) { // that'd be weird + $author = $original->getProfile(); + if (!empty($author)) { + $this->saveReply($author->id); + $replied[$author->id] = 1; + self::blow('reply:stream:%d', $author->id); + } + } + } + + // @todo ideally this parser information would only + // be calculated once. + + $mentions = common_find_mentions($this->content, $this); + + // store replied only for first @ (what user/notice what the reply directed, + // we assume first @ is it) + + foreach ($mentions as $mention) { + + foreach ($mention['mentioned'] as $mentioned) { + + // skip if they're already covered + + if (!empty($replied[$mentioned->id])) { + continue; + } + + // Don't save replies from blocked profile to local user + + $mentioned_user = User::staticGet('id', $mentioned->id); + if (!empty($mentioned_user) && $mentioned_user->hasBlocked($sender)) { + continue; + } + + $this->saveReply($mentioned->id); + $replied[$mentioned->id] = 1; + self::blow('reply:stream:%d', $mentioned->id); + } + } + + $recipientIds = array_keys($replied); + + return $recipientIds; + } + + function saveReply($profileId) + { + $reply = new Reply(); + + $reply->notice_id = $this->id; + $reply->profile_id = $profileId; + $reply->modified = $this->created; + + $reply->insert(); + + return $reply; + } + + protected $_replies = -1; + + /** + * Pull the complete list of @-reply targets for this notice. + * + * @return array of integer profile ids + */ + function getReplies() + { + if ($this->_replies != -1) { + return $this->_replies; + } + + $replyMap = Memcached_DataObject::listGet('Reply', 'notice_id', array($this->id)); + + $ids = array(); + + foreach ($replyMap[$this->id] as $reply) { + $ids[] = $reply->profile_id; + } + + $this->_replies = $ids; + + return $ids; + } + + function _setReplies($replies) + { + $this->_replies = $replies; + } + + /** + * Pull the complete list of @-reply targets for this notice. + * + * @return array of Profiles + */ + function getReplyProfiles() + { + $ids = $this->getReplies(); + + $profiles = Profile::multiGet('id', $ids); + + return $profiles->fetchAll(); + } + + /** + * Send e-mail notifications to local @-reply targets. + * + * Replies must already have been saved; this is expected to be run + * from the distrib queue handler. + */ + function sendReplyNotifications() + { + // Don't send reply notifications for repeats + + if (!empty($this->repeat_of)) { + return array(); + } + + $recipientIds = $this->getReplies(); + + foreach ($recipientIds as $recipientId) { + $user = User::staticGet('id', $recipientId); + if (!empty($user)) { + mail_notify_attn($user, $this); + } + } + } + + /** + * Pull list of groups this notice needs to be delivered to, + * as previously recorded by saveGroups() or saveKnownGroups(). + * + * @return array of Group objects + */ + + protected $_groups = -1; + + function getGroups() + { + // Don't save groups for repeats + + if (!empty($this->repeat_of)) { + return array(); + } + + if ($this->_groups != -1) + { + return $this->_groups; + } + + $gis = Memcached_DataObject::listGet('Group_inbox', 'notice_id', array($this->id)); + + $ids = array(); + + foreach ($gis[$this->id] as $gi) + { + $ids[] = $gi->group_id; + } + + $groups = User_group::multiGet('id', $ids); + + $this->_groups = $groups->fetchAll(); + + return $this->_groups; + } + + function _setGroups($groups) + { + $this->_groups = $groups; + } + + /** + * Convert a notice into an activity for export. + * + * @param User $cur Current user + * + * @return Activity activity object representing this Notice. + */ + + function asActivity($cur) + { + $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); + + if (!empty($act)) { + return $act; + } + $act = new Activity(); + + if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { + + $act->id = TagURI::mint("post:".$this->id); + $act->time = strtotime($this->created); + $act->content = common_xml_safe_str($this->rendered); + + $profile = $this->getProfile(); + + $act->actor = ActivityObject::fromProfile($profile); + $act->actor->extra[] = $profile->profileInfo($cur); + + $act->verb = $this->verb; + + if ($this->repeat_of) { + $repeated = Notice::staticGet('id', $this->repeat_of); + if (!empty($repeated)) { + $act->objects[] = $repeated->asActivity($cur); + } + } else { + $act->objects[] = ActivityObject::fromNotice($this); + } + + // XXX: should this be handled by default processing for object entry? + + // Categories + + $tags = $this->getTags(); + + foreach ($tags as $tag) { + $cat = new AtomCategory(); + $cat->term = $tag; + + $act->categories[] = $cat; + } + + // Enclosures + // XXX: use Atom Media and/or File activity objects instead + + $attachments = $this->attachments(); + + foreach ($attachments as $attachment) { + // Save local attachments + if (!empty($attachment->filename)) { + $act->attachments[] = ActivityObject::fromFile($attachment); + } + } + + $ctx = new ActivityContext(); + + if (!empty($this->reply_to)) { + $reply = Notice::staticGet('id', $this->reply_to); + if (!empty($reply)) { + $ctx->replyToID = $reply->uri; + $ctx->replyToUrl = $reply->bestUrl(); + } + } + + $ctx->location = $this->getLocation(); + + $conv = null; + + if (!empty($this->conversation)) { + $conv = Conversation::staticGet('id', $this->conversation); + if (!empty($conv)) { + $ctx->conversation = $conv->uri; + } + } + + $reply_ids = $this->getReplies(); + + foreach ($reply_ids as $id) { + $rprofile = Profile::staticGet('id', $id); + if (!empty($rprofile)) { + $ctx->attention[] = $rprofile->getUri(); + $ctx->attentionType[$rprofile->getUri()] = ActivityObject::PERSON; + } + } + + $groups = $this->getGroups(); + + foreach ($groups as $group) { + $ctx->attention[] = $group->getUri(); + $ctx->attentionType[$group->getUri()] = ActivityObject::GROUP; + } + + switch ($this->scope) { + case Notice::PUBLIC_SCOPE: + $ctx->attention[] = "http://activityschema.org/collection/public"; + $ctx->attentionType["http://activityschema.org/collection/public"] = ActivityObject::COLLECTION; + break; + case Notice::FOLLOWER_SCOPE: + $surl = common_local_url("subscribers", array('nickname' => $profile->nickname)); + $ctx->attention[] = $surl; + $ctx->attentionType[$surl] = ActivityObject::COLLECTION; + break; + } + + // XXX: deprecated; use ActivityVerb::SHARE instead + + $repeat = null; + + if (!empty($this->repeat_of)) { + $repeat = Notice::staticGet('id', $this->repeat_of); + if (!empty($repeat)) { + $ctx->forwardID = $repeat->uri; + $ctx->forwardUrl = $repeat->bestUrl(); + } + } + + $act->context = $ctx; + + $source = $this->getSource(); + + if ($source) { + $act->generator = ActivityObject::fromNoticeSource($source); + } + + // Source + + $atom_feed = $profile->getAtomFeed(); + + if (!empty($atom_feed)) { + + $act->source = new ActivitySource(); + + // XXX: we should store the actual feed ID + + $act->source->id = $atom_feed; + + // XXX: we should store the actual feed title + + $act->source->title = $profile->getBestName(); + + $act->source->links['alternate'] = $profile->profileurl; + $act->source->links['self'] = $atom_feed; + + $act->source->icon = $profile->avatarUrl(AVATAR_PROFILE_SIZE); + + $notice = $profile->getCurrentNotice(); + + if (!empty($notice)) { + $act->source->updated = self::utcDate($notice->created); + } + + $user = User::staticGet('id', $profile->id); + + if (!empty($user)) { + $act->source->links['license'] = common_config('license', 'url'); + } + } + + if ($this->isLocal()) { + $act->selfLink = common_local_url('ApiStatusesShow', array('id' => $this->id, + 'format' => 'atom')); + $act->editLink = $act->selfLink; + } + + Event::handle('EndNoticeAsActivity', array($this, &$act)); + } + + self::cacheSet(Cache::codeKey('notice:as-activity:'.$this->id), $act); + + return $act; + } + + // This has gotten way too long. Needs to be sliced up into functional bits + // or ideally exported to a utility class. + + function asAtomEntry($namespace=false, + $source=false, + $author=true, + $cur=null) + { + $act = $this->asActivity($cur); + $act->extra[] = $this->noticeInfo($cur); + return $act->asString($namespace, $author, $source); + } + + /** + * Extra notice info for atom entries + * + * Clients use some extra notice info in the atom stream. + * This gives it to them. + * + * @param User $cur Current user + * + * @return array representation of element + */ + + function noticeInfo($cur) + { + // local notice ID (useful to clients for ordering) + + $noticeInfoAttr = array('local_id' => $this->id); + + // notice source + + $ns = $this->getSource(); + + if (!empty($ns)) { + $noticeInfoAttr['source'] = $ns->code; + if (!empty($ns->url)) { + $noticeInfoAttr['source_link'] = $ns->url; + if (!empty($ns->name)) { + $noticeInfoAttr['source'] = '' + . htmlspecialchars($ns->name) + . ''; + } + } + } + + // favorite and repeated + + if (!empty($cur)) { + $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; + $cp = $cur->getProfile(); + $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this->id)) ? "true" : "false"; + } + + if (!empty($this->repeat_of)) { + $noticeInfoAttr['repeat_of'] = $this->repeat_of; + } + + return array('statusnet:notice_info', $noticeInfoAttr, null); + } + + /** + * Returns an XML string fragment with a reference to a notice as an + * Activity Streams noun object with the given element type. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @param string $element one of 'subject', 'object', 'target' + * @return string + */ + + function asActivityNoun($element) + { + $noun = ActivityObject::fromNotice($this); + return $noun->asString('activity:' . $element); + } + + function bestUrl() + { + if (!empty($this->url)) { + return $this->url; + } else if (!empty($this->uri) && preg_match('/^https?:/', $this->uri)) { + return $this->uri; + } else { + return common_local_url('shownotice', + array('notice' => $this->id)); + } + } + + + /** + * Determine which notice, if any, a new notice is in reply to. + * + * For conversation tracking, we try to see where this notice fits + * in the tree. Rough algorithm is: + * + * if (reply_to is set and valid) { + * return reply_to; + * } else if ((source not API or Web) and (content starts with "T NAME" or "@name ")) { + * return ID of last notice by initial @name in content; + * } + * + * Note that all @nickname instances will still be used to save "reply" records, + * so the notice shows up in the mentioned users' "replies" tab. + * + * @param integer $reply_to ID passed in by Web or API + * @param integer $profile_id ID of author + * @param string $source Source tag, like 'web' or 'gwibber' + * @param string $content Final notice content + * + * @return integer ID of replied-to notice, or null for not a reply. + */ + + static function getReplyTo($reply_to, $profile_id, $source, $content) + { + static $lb = array('xmpp', 'mail', 'sms', 'omb'); + + // If $reply_to is specified, we check that it exists, and then + // return it if it does + + if (!empty($reply_to)) { + $reply_notice = Notice::staticGet('id', $reply_to); + if (!empty($reply_notice)) { + return $reply_notice; + } + } + + // If it's not a "low bandwidth" source (one where you can't set + // a reply_to argument), we return. This is mostly web and API + // clients. + + if (!in_array($source, $lb)) { + return null; + } + + // Is there an initial @ or T? + + if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match) || + preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) { + $nickname = common_canonical_nickname($match[1]); + } else { + return null; + } + + // Figure out who that is. + + $sender = Profile::staticGet('id', $profile_id); + if (empty($sender)) { + return null; + } + + $recipient = common_relative_profile($sender, $nickname, common_sql_now()); + + if (empty($recipient)) { + return null; + } + + // Get their last notice + + $last = $recipient->getCurrentNotice(); + + if (!empty($last)) { + return $last; + } + + return null; + } + + static function maxContent() + { + $contentlimit = common_config('notice', 'contentlimit'); + // null => use global limit (distinct from 0!) + if (is_null($contentlimit)) { + $contentlimit = common_config('site', 'textlimit'); + } + return $contentlimit; + } + + static function contentTooLong($content) + { + $contentlimit = self::maxContent(); + return ($contentlimit > 0 && !empty($content) && (mb_strlen($content) > $contentlimit)); + } + + function getLocation() + { + $location = null; + + if (!empty($this->location_id) && !empty($this->location_ns)) { + $location = Location::fromId($this->location_id, $this->location_ns); + } + + if (is_null($location)) { // no ID, or Location::fromId() failed + if (!empty($this->lat) && !empty($this->lon)) { + $location = Location::fromLatLon($this->lat, $this->lon); + } + } + + return $location; + } + + /** + * Convenience function for posting a repeat of an existing message. + * + * @param int $repeater_id: profile ID of user doing the repeat + * @param string $source: posting source key, eg 'web', 'api', etc + * @return Notice + * + * @throws Exception on failure or permission problems + */ + function repeat($repeater_id, $source) + { + $author = Profile::staticGet('id', $this->profile_id); + + // TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'. + // TRANS: %1$s is the repeated user's name, %2$s is the repeated notice. + $content = sprintf(_('RT @%1$s %2$s'), + $author->nickname, + $this->content); + + $maxlen = common_config('site', 'textlimit'); + if ($maxlen > 0 && mb_strlen($content) > $maxlen) { + // Web interface and current Twitter API clients will + // pull the original notice's text, but some older + // clients and RSS/Atom feeds will see this trimmed text. + // + // Unfortunately this is likely to lose tags or URLs + // at the end of long notices. + $content = mb_substr($content, 0, $maxlen - 4) . ' ...'; + } + + // Scope is same as this one's + + return self::saveNew($repeater_id, + $content, + $source, + array('repeat_of' => $this->id, + 'scope' => $this->scope)); + } + + // These are supposed to be in chron order! + + function repeatStream($limit=100) + { + $cache = Cache::instance(); + + if (empty($cache)) { + $ids = $this->_repeatStreamDirect($limit); + } else { + $idstr = $cache->get(Cache::key('notice:repeats:'.$this->id)); + if ($idstr !== false) { + if (empty($idstr)) { + $ids = array(); + } else { + $ids = explode(',', $idstr); + } + } else { + $ids = $this->_repeatStreamDirect(100); + $cache->set(Cache::key('notice:repeats:'.$this->id), implode(',', $ids)); + } + if ($limit < 100) { + // We do a max of 100, so slice down to limit + $ids = array_slice($ids, 0, $limit); + } + } + + return NoticeStream::getStreamByIds($ids); + } + + function _repeatStreamDirect($limit) + { + $notice = new Notice(); + + $notice->selectAdd(); // clears it + $notice->selectAdd('id'); + + $notice->repeat_of = $this->id; + + $notice->orderBy('created, id'); // NB: asc! + + if (!is_null($limit)) { + $notice->limit(0, $limit); + } + + return $notice->fetchAll('id'); + } + + function locationOptions($lat, $lon, $location_id, $location_ns, $profile = null) + { + $options = array(); + + if (!empty($location_id) && !empty($location_ns)) { + $options['location_id'] = $location_id; + $options['location_ns'] = $location_ns; + + $location = Location::fromId($location_id, $location_ns); + + if (!empty($location)) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + } + + } else if (!empty($lat) && !empty($lon)) { + $options['lat'] = $lat; + $options['lon'] = $lon; + + $location = Location::fromLatLon($lat, $lon); + + if (!empty($location)) { + $options['location_id'] = $location->location_id; + $options['location_ns'] = $location->location_ns; + } + } else if (!empty($profile)) { + if (isset($profile->lat) && isset($profile->lon)) { + $options['lat'] = $profile->lat; + $options['lon'] = $profile->lon; + } + + if (isset($profile->location_id) && isset($profile->location_ns)) { + $options['location_id'] = $profile->location_id; + $options['location_ns'] = $profile->location_ns; + } + } + + return $options; + } + + function clearReplies() + { + $replyNotice = new Notice(); + $replyNotice->reply_to = $this->id; + + //Null any notices that are replies to this notice + + if ($replyNotice->find()) { + while ($replyNotice->fetch()) { + $orig = clone($replyNotice); + $replyNotice->reply_to = null; + $replyNotice->update($orig); + } + } + + // Reply records + + $reply = new Reply(); + $reply->notice_id = $this->id; + + if ($reply->find()) { + while($reply->fetch()) { + self::blow('reply:stream:%d', $reply->profile_id); + $reply->delete(); + } + } + + $reply->free(); + } + + function clearFiles() + { + $f2p = new File_to_post(); + + $f2p->post_id = $this->id; + + if ($f2p->find()) { + while ($f2p->fetch()) { + $f2p->delete(); + } + } + // FIXME: decide whether to delete File objects + // ...and related (actual) files + } + + function clearRepeats() + { + $repeatNotice = new Notice(); + $repeatNotice->repeat_of = $this->id; + + //Null any notices that are repeats of this notice + + if ($repeatNotice->find()) { + while ($repeatNotice->fetch()) { + $orig = clone($repeatNotice); + $repeatNotice->repeat_of = null; + $repeatNotice->update($orig); + } + } + } + + function clearFaves() + { + $fave = new Fave(); + $fave->notice_id = $this->id; + + if ($fave->find()) { + while ($fave->fetch()) { + self::blow('fave:ids_by_user_own:%d', $fave->user_id); + self::blow('fave:ids_by_user_own:%d;last', $fave->user_id); + self::blow('fave:ids_by_user:%d', $fave->user_id); + self::blow('fave:ids_by_user:%d;last', $fave->user_id); + $fave->delete(); + } + } + + $fave->free(); + } + + function clearTags() + { + $tag = new Notice_tag(); + $tag->notice_id = $this->id; + + if ($tag->find()) { + while ($tag->fetch()) { + self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, Cache::keyize($tag->tag)); + self::blow('profile:notice_ids_tagged:%d:%s;last', $this->profile_id, Cache::keyize($tag->tag)); + self::blow('notice_tag:notice_ids:%s', Cache::keyize($tag->tag)); + self::blow('notice_tag:notice_ids:%s;last', Cache::keyize($tag->tag)); + $tag->delete(); + } + } + + $tag->free(); + } + + function clearGroupInboxes() + { + $gi = new Group_inbox(); + + $gi->notice_id = $this->id; + + if ($gi->find()) { + while ($gi->fetch()) { + self::blow('user_group:notice_ids:%d', $gi->group_id); + $gi->delete(); + } + } + + $gi->free(); + } + + function distribute() + { + // We always insert for the author so they don't + // have to wait + Event::handle('StartNoticeDistribute', array($this)); + + $user = User::staticGet('id', $this->profile_id); + if (!empty($user)) { + Inbox::insertNotice($user->id, $this->id); + } + + if (common_config('queue', 'inboxes')) { + // If there's a failure, we want to _force_ + // distribution at this point. + try { + $qm = QueueManager::get(); + $qm->enqueue($this, 'distrib'); + } catch (Exception $e) { + // If the exception isn't transient, this + // may throw more exceptions as DQH does + // its own enqueueing. So, we ignore them! + try { + $handler = new DistribQueueHandler(); + $handler->handle($this); + } catch (Exception $e) { + common_log(LOG_ERR, "emergency redistribution resulted in " . $e->getMessage()); + } + // Re-throw so somebody smarter can handle it. + throw $e; + } + } else { + $handler = new DistribQueueHandler(); + $handler->handle($this); + } + } + + function insert() + { + $result = parent::insert(); + + if ($result) { + // Profile::hasRepeated() abuses pkeyGet(), so we + // have to clear manually + if (!empty($this->repeat_of)) { + $c = self::memcache(); + if (!empty($c)) { + $ck = self::multicacheKey('Notice', + array('profile_id' => $this->profile_id, + 'repeat_of' => $this->repeat_of)); + $c->delete($ck); + } + } + } + + return $result; + } + + /** + * Get the source of the notice + * + * @return Notice_source $ns A notice source object. 'code' is the only attribute + * guaranteed to be populated. + */ + function getSource() + { + $ns = new Notice_source(); + if (!empty($this->source)) { + switch ($this->source) { + case 'web': + case 'xmpp': + case 'mail': + case 'omb': + case 'system': + case 'api': + $ns->code = $this->source; + break; + default: + $ns = Notice_source::staticGet($this->source); + if (!$ns) { + $ns = new Notice_source(); + $ns->code = $this->source; + $app = Oauth_application::staticGet('name', $this->source); + if ($app) { + $ns->name = $app->name; + $ns->url = $app->source_url; + } + } + break; + } + } + return $ns; + } + + /** + * Determine whether the notice was locally created + * + * @return boolean locality + */ + + public function isLocal() + { + return ($this->is_local == Notice::LOCAL_PUBLIC || + $this->is_local == Notice::LOCAL_NONPUBLIC); + } + + /** + * Get the list of hash tags saved with this notice. + * + * @return array of strings + */ + public function getTags() + { + $tags = array(); + + $keypart = sprintf('notice:tags:%d', $this->id); + + $tagstr = self::cacheGet($keypart); + + if ($tagstr !== false) { + $tags = explode(',', $tagstr); + } else { + $tag = new Notice_tag(); + $tag->notice_id = $this->id; + if ($tag->find()) { + while ($tag->fetch()) { + $tags[] = $tag->tag; + } + } + self::cacheSet($keypart, implode(',', $tags)); + } + + return $tags; + } + + static private function utcDate($dt) + { + $dateStr = date('d F Y H:i:s', strtotime($dt)); + $d = new DateTime($dateStr, new DateTimeZone('UTC')); + return $d->format(DATE_W3C); + } + + /** + * Look up the creation timestamp for a given notice ID, even + * if it's been deleted. + * + * @param int $id + * @return mixed string recorded creation timestamp, or false if can't be found + */ + public static function getAsTimestamp($id) + { + if (!$id) { + return false; + } + + $notice = Notice::staticGet('id', $id); + if ($notice) { + return $notice->created; + } + + $deleted = Deleted_notice::staticGet('id', $id); + if ($deleted) { + return $deleted->created; + } + + return false; + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a since_id + * parameter, matching notices posted after the given one (exclusive). + * + * If the referenced notice can't be found, will return false. + * + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function whereSinceId($id, $idField='id', $createdField='created') + { + $since = Notice::getAsTimestamp($id); + if ($since) { + return sprintf("($createdField = '%s' and $idField > %d) or ($createdField > '%s')", $since, $id, $since); + } + return false; + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a since_id + * parameter, matching notices posted after the given one (exclusive), and + * if necessary add it to the data object's query. + * + * @param DB_DataObject $obj + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function addWhereSinceId(DB_DataObject $obj, $id, $idField='id', $createdField='created') + { + $since = self::whereSinceId($id, $idField, $createdField); + if ($since) { + $obj->whereAdd($since); + } + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a max_id + * parameter, matching notices posted before the given one (inclusive). + * + * If the referenced notice can't be found, will return false. + * + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function whereMaxId($id, $idField='id', $createdField='created') + { + $max = Notice::getAsTimestamp($id); + if ($max) { + return sprintf("($createdField < '%s') or ($createdField = '%s' and $idField <= %d)", $max, $max, $id); + } + return false; + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a max_id + * parameter, matching notices posted before the given one (inclusive), and + * if necessary add it to the data object's query. + * + * @param DB_DataObject $obj + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function addWhereMaxId(DB_DataObject $obj, $id, $idField='id', $createdField='created') + { + $max = self::whereMaxId($id, $idField, $createdField); + if ($max) { + $obj->whereAdd($max); + } + } + + function isPublic() + { + if (common_config('public', 'localonly')) { + return ($this->is_local == Notice::LOCAL_PUBLIC); + } else { + return (($this->is_local != Notice::LOCAL_NONPUBLIC) && + ($this->is_local != Notice::GATEWAY)); + } + } + + /** + * Check that the given profile is allowed to read, respond to, or otherwise + * act on this notice. + * + * The $scope member is a bitmask of scopes, representing a logical AND of the + * scope requirement. So, 0x03 (Notice::ADDRESSEE_SCOPE | Notice::SITE_SCOPE) means + * "only visible to people who are mentioned in the notice AND are users on this site." + * Users on the site who are not mentioned in the notice will not be able to see the + * notice. + * + * @param Profile $profile The profile to check; pass null to check for public/unauthenticated users. + * + * @return boolean whether the profile is in the notice's scope + */ + function inScope($profile) + { + if (is_null($profile)) { + $keypart = sprintf('notice:in-scope-for:%d:null', $this->id); + } else { + $keypart = sprintf('notice:in-scope-for:%d:%d', $this->id, $profile->id); + } + + $result = self::cacheGet($keypart); + + if ($result === false) { + $bResult = false; + if (Event::handle('StartNoticeInScope', array($this, $profile, &$bResult))) { + $bResult = $this->_inScope($profile); + Event::handle('EndNoticeInScope', array($this, $profile, &$bResult)); + } + $result = ($bResult) ? 1 : 0; + self::cacheSet($keypart, $result, 0, 300); + } + + return ($result == 1) ? true : false; + } + + protected function _inScope($profile) + { + if (!is_null($this->scope)) { + $scope = $this->scope; + } else { + $scope = self::defaultScope(); + } + + // If there's no scope, anyone (even anon) is in scope. + + if ($scope == 0) { // Not private + + return !$this->isHiddenSpam($profile); + + } else { // Private, somehow + + // If there's scope, anon cannot be in scope + + if (empty($profile)) { + return false; + } + + // Author is always in scope + + if ($this->profile_id == $profile->id) { + return true; + } + + // Only for users on this site + + if ($scope & Notice::SITE_SCOPE) { + $user = $profile->getUser(); + if (empty($user)) { + return false; + } + } + + // Only for users mentioned in the notice + + if ($scope & Notice::ADDRESSEE_SCOPE) { + + $repl = Reply::pkeyGet(array('notice_id' => $this->id, + 'profile_id' => $profile->id)); + + if (empty($repl)) { + return false; + } + } + + // Only for members of the given group + + if ($scope & Notice::GROUP_SCOPE) { + + // XXX: just query for the single membership + + $groups = $this->getGroups(); + + $foundOne = false; + + foreach ($groups as $group) { + if ($profile->isMember($group)) { + $foundOne = true; + break; + } + } + + if (!$foundOne) { + return false; + } + } + + // Only for followers of the author + + $author = null; + + if ($scope & Notice::FOLLOWER_SCOPE) { + + try { + $author = $this->getProfile(); + } catch (Exception $e) { + return false; + } + + if (!Subscription::exists($profile, $author)) { + return false; + } + } + + return !$this->isHiddenSpam($profile); + } + } + + function isHiddenSpam($profile) { + + // Hide posts by silenced users from everyone but moderators. + + if (common_config('notice', 'hidespam')) { + + try { + $author = $this->getProfile(); + } catch(Exception $e) { + // If we can't get an author, keep it hidden. + // XXX: technically not spam, but, whatever. + return true; + } + + if ($author->hasRole(Profile_role::SILENCED)) { + if (empty($profile) || (($profile->id !== $author->id) && (!$profile->hasRight(Right::REVIEWSPAM)))) { + return true; + } + } + } + + return false; + } + + static function groupsFromText($text, $profile) + { + $groups = array(); + + /* extract all !group */ + $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/', + strtolower($text), + $match); + + if (!$count) { + return $groups; + } + + foreach (array_unique($match[1]) as $nickname) { + $group = User_group::getForNickname($nickname, $profile); + if (!empty($group) && $profile->isMember($group)) { + $groups[] = $group->id; + } + } + + return $groups; + } + + protected $_original = -1; + + function getOriginal() + { + if (is_int($this->_original) && $this->_original == -1) { + if (empty($this->reply_to)) { + $this->_original = null; + } else { + $this->_original = Notice::staticGet('id', $this->reply_to); + } + } + return $this->_original; + } + + /** + * Magic function called at serialize() time. + * + * We use this to drop a couple process-specific references + * from DB_DataObject which can cause trouble in future + * processes. + * + * @return array of variable names to include in serialization. + */ + + function __sleep() + { + $vars = parent::__sleep(); + $skip = array('_original', '_profile', '_groups', '_attachments', '_faves', '_replies', '_repeats'); + return array_diff($vars, $skip); + } + + static function defaultScope() + { + $scope = common_config('notice', 'defaultscope'); + if (is_null($scope)) { + if (common_config('site', 'private')) { + $scope = 1; + } else { + $scope = 0; + } + } + return $scope; + } + + static function fillProfiles($notices) + { + $map = self::getProfiles($notices); + + foreach ($notices as $notice) { + if (array_key_exists($notice->profile_id, $map)) { + $notice->_setProfile($map[$notice->profile_id]); + } + } + + return array_values($map); + } + + static function getProfiles(&$notices) + { + $ids = array(); + foreach ($notices as $notice) { + $ids[] = $notice->profile_id; + } + + $ids = array_unique($ids); + + return Memcached_DataObject::pivotGet('Profile', 'id', $ids); + } + + static function fillGroups(&$notices) + { + $ids = self::_idsOf($notices); + + $gis = Memcached_DataObject::listGet('Group_inbox', 'notice_id', $ids); + + $gids = array(); + + foreach ($gis as $id => $gi) + { + foreach ($gi as $g) + { + $gids[] = $g->group_id; + } + } + + $gids = array_unique($gids); + + $group = Memcached_DataObject::pivotGet('User_group', 'id', $gids); + + foreach ($notices as $notice) + { + $grps = array(); + $gi = $gis[$notice->id]; + foreach ($gi as $g) { + $grps[] = $group[$g->group_id]; + } + $notice->_setGroups($grps); + } + } + + static function _idsOf(&$notices) + { + $ids = array(); + foreach ($notices as $notice) { + $ids[] = $notice->id; + } + $ids = array_unique($ids); + return $ids; + } + + static function fillAttachments(&$notices) + { + $ids = self::_idsOf($notices); + + $f2pMap = Memcached_DataObject::listGet('File_to_post', 'post_id', $ids); + + $fileIds = array(); + + foreach ($f2pMap as $noticeId => $f2ps) { + foreach ($f2ps as $f2p) { + $fileIds[] = $f2p->file_id; + } + } + + $fileIds = array_unique($fileIds); + + $fileMap = Memcached_DataObject::pivotGet('File', 'id', $fileIds); + + foreach ($notices as $notice) + { + $files = array(); + $f2ps = $f2pMap[$notice->id]; + foreach ($f2ps as $f2p) { + $files[] = $fileMap[$f2p->file_id]; + } + $notice->_setAttachments($files); + } + } + + protected $_faves; + + /** + * All faves of this notice + * + * @return array Array of Fave objects + */ + + function getFaves() + { + if (isset($this->_faves) && is_array($this->_faves)) { + return $this->_faves; + } + $faveMap = Memcached_DataObject::listGet('Fave', 'notice_id', array($this->id)); + $this->_faves = $faveMap[$this->id]; + return $this->_faves; + } + + function _setFaves($faves) + { + $this->_faves = $faves; + } + + static function fillFaves(&$notices) + { + $ids = self::_idsOf($notices); + $faveMap = Memcached_DataObject::listGet('Fave', 'notice_id', $ids); + $cnt = 0; + $faved = array(); + foreach ($faveMap as $id => $faves) { + $cnt += count($faves); + if (count($faves) > 0) { + $faved[] = $id; + } + } + foreach ($notices as $notice) { + $faves = $faveMap[$notice->id]; + $notice->_setFaves($faves); + } + } + + static function fillReplies(&$notices) + { + $ids = self::_idsOf($notices); + $replyMap = Memcached_DataObject::listGet('Reply', 'notice_id', $ids); + foreach ($notices as $notice) { + $replies = $replyMap[$notice->id]; + $ids = array(); + foreach ($replies as $reply) { + $ids[] = $reply->profile_id; + } + $notice->_setReplies($ids); + } + } + + protected $_repeats; + + function getRepeats() + { + if (isset($this->_repeats) && is_array($this->_repeats)) { + return $this->_repeats; + } + $repeatMap = Memcached_DataObject::listGet('Notice', 'repeat_of', array($this->id)); + $this->_repeats = $repeatMap[$this->id]; + return $this->_repeats; + } + + function _setRepeats($repeats) + { + $this->_repeats = $repeats; + } + + static function fillRepeats(&$notices) + { + $ids = self::_idsOf($notices); + $repeatMap = Memcached_DataObject::listGet('Notice', 'repeat_of', $ids); + foreach ($notices as $notice) { + $repeats = $repeatMap[$notice->id]; + $notice->_setRepeats($repeats); + } + } +} diff --git a/api-changes-1.1.1/lib/apiaction.php b/api-changes-1.1.1/lib/apiaction.php new file mode 100644 index 0000000..350441b --- /dev/null +++ b/api-changes-1.1.1/lib/apiaction.php @@ -0,0 +1,1684 @@ +. + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Dan Moore + * @author Evan Prodromou + * @author Jeffery To + * @author Toby Inkster + * @author Zach Copley + * @copyright 2009-2010 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +/* External API usage documentation. Please update when you change how the API works. */ + +/*! @mainpage StatusNet REST API + + @section Introduction + + Some explanatory text about the API would be nice. + + @section API Methods + + @subsection timelinesmethods_sec Timeline Methods + + @li @ref publictimeline + @li @ref friendstimeline + + @subsection statusmethods_sec Status Methods + + @li @ref statusesupdate + + @subsection usermethods_sec User Methods + + @subsection directmessagemethods_sec Direct Message Methods + + @subsection friendshipmethods_sec Friendship Methods + + @subsection socialgraphmethods_sec Social Graph Methods + + @subsection accountmethods_sec Account Methods + + @subsection favoritesmethods_sec Favorites Methods + + @subsection blockmethods_sec Block Methods + + @subsection oauthmethods_sec OAuth Methods + + @subsection helpmethods_sec Help Methods + + @subsection groupmethods_sec Group Methods + + @page apiroot API Root + + The URLs for methods referred to in this API documentation are + relative to the StatusNet API root. The API root is determined by the + site's @b server and @b path variables, which are generally specified + in config.php. For example: + + @code + $config['site']['server'] = 'example.org'; + $config['site']['path'] = 'statusnet' + @endcode + + The pattern for a site's API root is: @c protocol://server/path/api E.g: + + @c http://example.org/statusnet/api + + The @b path can be empty. In that case the API root would simply be: + + @c http://example.org/api + +*/ + +if (!defined('STATUSNET')) { + exit(1); +} + +class ApiValidationException extends Exception { } + +/** + * Contains most of the Twitter-compatible API output functions. + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Dan Moore + * @author Evan Prodromou + * @author Jeffery To + * @author Toby Inkster + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class ApiAction extends Action +{ + const READ_ONLY = 1; + const READ_WRITE = 2; + + var $format = null; + var $user = null; + var $auth_user = null; + var $page = null; + var $count = null; + var $max_id = null; + var $since_id = null; + var $source = null; + var $callback = null; + + var $access = self::READ_ONLY; // read (default) or read-write + + static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api'); + + /** + * Initialization. + * + * @param array $args Web and URL arguments + * + * @return boolean false if user doesn't exist + */ + function prepare($args) + { + StatusNet::setApi(true); // reduce exception reports to aid in debugging + parent::prepare($args); + + $this->format = $this->arg('format'); + $this->callback = $this->arg('callback'); + $this->page = (int)$this->arg('page', 1); + $this->count = (int)$this->arg('count', 20); + $this->max_id = (int)$this->arg('max_id', 0); + $this->since_id = (int)$this->arg('since_id', 0); + + if ($this->arg('since')) { + header('X-StatusNet-Warning: since parameter is disabled; use since_id'); + } + + $this->source = $this->trimmed('source'); + + if (empty($this->source) || in_array($this->source, self::$reserved_sources)) { + $this->source = 'api'; + } + + return true; + } + + /** + * Handle a request + * + * @param array $args Arguments from $_REQUEST + * + * @return void + */ + function handle($args) + { + header('Access-Control-Allow-Origin: *'); + parent::handle($args); + } + + /** + * Overrides XMLOutputter::element to write booleans as strings (true|false). + * See that method's documentation for more info. + * + * @param string $tag Element type or tagname + * @param array $attrs Array of element attributes, as + * key-value pairs + * @param string $content string content of the element + * + * @return void + */ + function element($tag, $attrs=null, $content=null) + { + if (is_bool($content)) { + $content = ($content ? 'true' : 'false'); + } + + return parent::element($tag, $attrs, $content); + } + + function twitterUserArray($profile, $get_notice=false) + { + $twitter_user = array(); + + $user = $profile->getUser(); + + $twitter_user['id'] = intval($profile->id); + $twitter_user['name'] = $profile->getBestName(); + $twitter_user['screen_name'] = $profile->nickname; + $twitter_user['location'] = ($profile->location) ? $profile->location : null; + $twitter_user['description'] = ($profile->bio) ? $profile->bio : null; + + $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE); + $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() : + Avatar::defaultImage(AVATAR_STREAM_SIZE); + $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + $twitter_user['profile_image_url_profile_size'] = ($avatar) ? $avatar->displayUrl() : + Avatar::defaultImage(AVATAR_PROFILE_SIZE); + $avatar = $profile->getOriginalAvatar(); + $twitter_user['profile_image_url_original'] = ($avatar) ? $avatar->displayUrl() : + Avatar::defaultImage(AVATAR_PROFILE_SIZE); + + $groups = $profile->getGroups(); + $groups_count = 0; while($groups->fetch()) $groups_count++; + $twitter_user['groups_count'] = $groups_count; + + $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null; + $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false; + $twitter_user['followers_count'] = $profile->subscriberCount(); + + // Note: some profiles don't have an associated user + + $twitter_user['friends_count'] = $profile->subscriptionCount(); + + $twitter_user['created_at'] = $this->dateTwitter($profile->created); + + $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling! + + $timezone = 'UTC'; + + if (!empty($user) && $user->timezone) { + $timezone = $user->timezone; + } + + $t = new DateTime; + $t->setTimezone(new DateTimeZone($timezone)); + + $twitter_user['utc_offset'] = $t->format('Z'); + $twitter_user['time_zone'] = $timezone; + $twitter_user['statuses_count'] = $profile->noticeCount(); + + // Is the requesting user following this user? + $twitter_user['following'] = false; + $twitter_user['statusnet_blocking'] = false; + $twitter_user['notifications'] = false; + + if (isset($this->auth_user)) { + + $twitter_user['following'] = $this->auth_user->isSubscribed($profile); + $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile); + + // Notifications on? + $sub = Subscription::pkeyGet(array('subscriber' => + $this->auth_user->id, + 'subscribed' => $profile->id)); + + if ($sub) { + $twitter_user['notifications'] = ($sub->jabber || $sub->sms); + } + } + + if ($get_notice) { + $notice = $profile->getCurrentNotice(); + if ($notice) { + // don't get user! + $twitter_user['status'] = $this->twitterStatusArray($notice, false); + } + } + + // StatusNet-specific + + $twitter_user['statusnet_profile_url'] = $profile->profileurl; + + return $twitter_user; + } + + function twitterStatusArray($notice, $include_user=true) + { + $base = $this->twitterSimpleStatusArray($notice, $include_user); + + if (!empty($notice->repeat_of)) { + $original = Notice::staticGet('id', $notice->repeat_of); + if (!empty($original)) { + $original_array = $this->twitterSimpleStatusArray($original, $include_user); + $base['retweeted_status'] = $original_array; + } + } + + return $base; + } + + function twitterSimpleStatusArray($notice, $include_user=true) + { + $profile = $notice->getProfile(); + + $twitter_status = array(); + $twitter_status['text'] = $notice->content; + $twitter_status['truncated'] = false; # Not possible on StatusNet + $twitter_status['created_at'] = $this->dateTwitter($notice->created); + $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ? + intval($notice->reply_to) : null; + + $source = null; + + $ns = $notice->getSource(); + if ($ns) { + if (!empty($ns->name) && !empty($ns->url)) { + $source = '' + . htmlspecialchars($ns->name) + . ''; + } else { + $source = $ns->code; + } + } + + $twitter_status['source'] = $source; + $twitter_status['id'] = intval($notice->id); + $twitter_status['uri'] = $notice->uri; + + $replier_profile = null; + + if ($notice->reply_to) { + $reply = Notice::staticGet(intval($notice->reply_to)); + if ($reply) { + $replier_profile = $reply->getProfile(); + } + } + + $twitter_status['in_reply_to_user_id'] = + ($replier_profile) ? intval($replier_profile->id) : null; + $twitter_status['in_reply_to_screen_name'] = + ($replier_profile) ? $replier_profile->nickname : null; + + if (isset($notice->lat) && isset($notice->lon)) { + // This is the format that GeoJSON expects stuff to be in + $twitter_status['geo'] = array('type' => 'Point', + 'coordinates' => array((float) $notice->lat, + (float) $notice->lon)); + } else { + $twitter_status['geo'] = null; + } + + if (isset($this->auth_user)) { + $this_profile = $this->auth_user->getProfile(); + $twitter_status['favorited'] = $this->auth_user->hasFave($notice); + $twitter_status['repeated'] = $this_profile->hasRepeated($notice->id); + } else { + $twitter_status['favorited'] = false; + $twitter_status['repeated'] = false; + } + + // Enclosures + $attachments = $notice->attachments(); + + if (!empty($attachments)) { + + $twitter_status['attachments'] = array(); + + foreach ($attachments as $attachment) { + $enclosure_o=$attachment->getEnclosure(); + if ($enclosure_o) { + $enclosure = array(); + $enclosure['url'] = $enclosure_o->url; + $enclosure['mimetype'] = $enclosure_o->mimetype; + $enclosure['size'] = $enclosure_o->size; + $twitter_status['attachments'][] = $enclosure; + } + } + } + + if ($include_user && $profile) { + // Don't get notice (recursive!) + $twitter_user = $this->twitterUserArray($profile, false); + $twitter_status['user'] = $twitter_user; + } + + // StatusNet-specific + + $twitter_status['statusnet_html'] = $notice->rendered; + $twitter_status['statusnet_conversation_id'] = intval($notice->conversation); + + return $twitter_status; + } + + function twitterGroupArray($group) + { + $twitter_group = array(); + + $twitter_group['id'] = intval($group->id); + $twitter_group['url'] = $group->permalink(); + $twitter_group['nickname'] = $group->nickname; + $twitter_group['fullname'] = $group->fullname; + + if (isset($this->auth_user)) { + $twitter_group['member'] = $this->auth_user->isMember($group); + $twitter_group['blocked'] = Group_block::isBlocked( + $group, + $this->auth_user->getProfile() + ); + } + + $admins = $group->getAdmins(); + $admin_count = 0; while($admins->fetch()) $admin_count++; + $twitter_group['admin_count'] = $admin_count; + $twitter_group['member_count'] = $group->getMemberCount(); + $twitter_group['original_logo'] = $group->original_logo; + $twitter_group['homepage_logo'] = $group->homepage_logo; + $twitter_group['stream_logo'] = $group->stream_logo; + $twitter_group['mini_logo'] = $group->mini_logo; + $twitter_group['homepage'] = $group->homepage; + $twitter_group['description'] = $group->description; + $twitter_group['location'] = $group->location; + $twitter_group['created'] = $this->dateTwitter($group->created); + $twitter_group['modified'] = $this->dateTwitter($group->modified); + + return $twitter_group; + } + + function twitterRssGroupArray($group) + { + $entry = array(); + $entry['content']=$group->description; + $entry['title']=$group->nickname; + $entry['link']=$group->permalink(); + $entry['published']=common_date_iso8601($group->created); + $entry['updated']==common_date_iso8601($group->modified); + $taguribase = common_config('integration', 'groupuri'); + $entry['id'] = "group:$groupuribase:$entry[link]"; + + $entry['description'] = $entry['content']; + $entry['pubDate'] = common_date_rfc2822($group->created); + $entry['guid'] = $entry['link']; + + return $entry; + } + + function twitterListArray($list) + { + $profile = Profile::staticGet('id', $list->tagger); + + $twitter_list = array(); + $twitter_list['id'] = $list->id; + $twitter_list['name'] = $list->tag; + $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;; + $twitter_list['slug'] = $list->tag; + $twitter_list['description'] = $list->description; + $twitter_list['subscriber_count'] = $list->subscriberCount(); + $twitter_list['member_count'] = $list->taggedCount(); + $twitter_list['uri'] = $list->getUri(); + + if (isset($this->auth_user)) { + $twitter_list['following'] = $list->hasSubscriber($this->auth_user); + } else { + $twitter_list['following'] = false; + } + + $twitter_list['mode'] = ($list->private) ? 'private' : 'public'; + $twitter_list['user'] = $this->twitterUserArray($profile, false); + + return $twitter_list; + } + + function twitterRssEntryArray($notice) + { + $entry = array(); + + if (Event::handle('StartRssEntryArray', array($notice, &$entry))) { + $profile = $notice->getProfile(); + + // We trim() to avoid extraneous whitespace in the output + + $entry['content'] = common_xml_safe_str(trim($notice->rendered)); + $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content)); + $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id)); + $entry['published'] = common_date_iso8601($notice->created); + + $taguribase = TagURI::base(); + $entry['id'] = "tag:$taguribase:$entry[link]"; + + $entry['updated'] = $entry['published']; + $entry['author'] = $profile->getBestName(); + + // Enclosures + $attachments = $notice->attachments(); + $enclosures = array(); + + foreach ($attachments as $attachment) { + $enclosure_o=$attachment->getEnclosure(); + if ($enclosure_o) { + $enclosure = array(); + $enclosure['url'] = $enclosure_o->url; + $enclosure['mimetype'] = $enclosure_o->mimetype; + $enclosure['size'] = $enclosure_o->size; + $enclosures[] = $enclosure; + } + } + + if (!empty($enclosures)) { + $entry['enclosures'] = $enclosures; + } + + // Tags/Categories + $tag = new Notice_tag(); + $tag->notice_id = $notice->id; + if ($tag->find()) { + $entry['tags']=array(); + while ($tag->fetch()) { + $entry['tags'][]=$tag->tag; + } + } + $tag->free(); + + // RSS Item specific + $entry['description'] = $entry['content']; + $entry['pubDate'] = common_date_rfc2822($notice->created); + $entry['guid'] = $entry['link']; + + if (isset($notice->lat) && isset($notice->lon)) { + // This is the format that GeoJSON expects stuff to be in. + // showGeoRSS() below uses it for XML output, so we reuse it + $entry['geo'] = array('type' => 'Point', + 'coordinates' => array((float) $notice->lat, + (float) $notice->lon)); + } else { + $entry['geo'] = null; + } + + Event::handle('EndRssEntryArray', array($notice, &$entry)); + } + + return $entry; + } + + function twitterRelationshipArray($source, $target) + { + $relationship = array(); + + $relationship['source'] = + $this->relationshipDetailsArray($source, $target); + $relationship['target'] = + $this->relationshipDetailsArray($target, $source); + + return array('relationship' => $relationship); + } + + function relationshipDetailsArray($source, $target) + { + $details = array(); + + $details['screen_name'] = $source->nickname; + $details['followed_by'] = $target->isSubscribed($source); + $details['following'] = $source->isSubscribed($target); + + $notifications = false; + + if ($source->isSubscribed($target)) { + $sub = Subscription::pkeyGet(array('subscriber' => + $source->id, 'subscribed' => $target->id)); + + if (!empty($sub)) { + $notifications = ($sub->jabber || $sub->sms); + } + } + + $details['notifications_enabled'] = $notifications; + $details['blocking'] = $source->hasBlocked($target); + $details['id'] = intval($source->id); + + return $details; + } + + function showTwitterXmlRelationship($relationship) + { + $this->elementStart('relationship'); + + foreach($relationship as $element => $value) { + if ($element == 'source' || $element == 'target') { + $this->elementStart($element); + $this->showXmlRelationshipDetails($value); + $this->elementEnd($element); + } + } + + $this->elementEnd('relationship'); + } + + function showXmlRelationshipDetails($details) + { + foreach($details as $element => $value) { + $this->element($element, null, $value); + } + } + + function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false) + { + $attrs = array(); + if ($namespaces) { + $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/'; + } + $this->elementStart($tag, $attrs); + foreach($twitter_status as $element => $value) { + switch ($element) { + case 'user': + $this->showTwitterXmlUser($twitter_status['user']); + break; + case 'text': + $this->element($element, null, common_xml_safe_str($value)); + break; + case 'attachments': + $this->showXmlAttachments($twitter_status['attachments']); + break; + case 'geo': + $this->showGeoXML($value); + break; + case 'retweeted_status': + $this->showTwitterXmlStatus($value, 'retweeted_status'); + break; + default: + if (strncmp($element, 'statusnet_', 10) == 0) { + $this->element('statusnet:'.substr($element, 10), null, $value); + } else { + $this->element($element, null, $value); + } + } + } + $this->elementEnd($tag); + } + + function showTwitterXmlGroup($twitter_group) + { + $this->elementStart('group'); + foreach($twitter_group as $element => $value) { + $this->element($element, null, $value); + } + $this->elementEnd('group'); + } + + function showTwitterXmlList($twitter_list) + { + $this->elementStart('list'); + foreach($twitter_list as $element => $value) { + if($element == 'user') { + $this->showTwitterXmlUser($value, 'user'); + } + else { + $this->element($element, null, $value); + } + } + $this->elementEnd('list'); + } + + function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false) + { + $attrs = array(); + if ($namespaces) { + $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/'; + } + $this->elementStart($role, $attrs); + foreach($twitter_user as $element => $value) { + if ($element == 'status') { + $this->showTwitterXmlStatus($twitter_user['status']); + } else if (strncmp($element, 'statusnet_', 10) == 0) { + $this->element('statusnet:'.substr($element, 10), null, $value); + } else { + $this->element($element, null, $value); + } + } + $this->elementEnd($role); + } + + function showXmlAttachments($attachments) { + if (!empty($attachments)) { + $this->elementStart('attachments', array('type' => 'array')); + foreach ($attachments as $attachment) { + $attrs = array(); + $attrs['url'] = $attachment['url']; + $attrs['mimetype'] = $attachment['mimetype']; + $attrs['size'] = $attachment['size']; + $this->element('enclosure', $attrs, ''); + } + $this->elementEnd('attachments'); + } + } + + function showGeoXML($geo) + { + if (empty($geo)) { + // empty geo element + $this->element('geo'); + } else { + $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss')); + $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]); + $this->elementEnd('geo'); + } + } + + function showGeoRSS($geo) + { + if (!empty($geo)) { + $this->element( + 'georss:point', + null, + $geo['coordinates'][0] . ' ' . $geo['coordinates'][1] + ); + } + } + + function showTwitterRssItem($entry) + { + $this->elementStart('item'); + $this->element('title', null, $entry['title']); + $this->element('description', null, $entry['description']); + $this->element('pubDate', null, $entry['pubDate']); + $this->element('guid', null, $entry['guid']); + $this->element('link', null, $entry['link']); + + // RSS only supports 1 enclosure per item + if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){ + $enclosure = $entry['enclosures'][0]; + $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null); + } + + if(array_key_exists('tags', $entry)){ + foreach($entry['tags'] as $tag){ + $this->element('category', null,$tag); + } + } + + $this->showGeoRSS($entry['geo']); + $this->elementEnd('item'); + } + + function showJsonObjects($objects) + { + print(json_encode($objects)); + } + + function showSingleXmlStatus($notice) + { + $this->initDocument('xml'); + $twitter_status = $this->twitterStatusArray($notice); + $this->showTwitterXmlStatus($twitter_status, 'status', true); + $this->endDocument('xml'); + } + + function showSingleAtomStatus($notice) + { + header('Content-Type: application/atom+xml; charset=utf-8'); + print $notice->asAtomEntry(true, true, true, $this->auth_user); + } + + function show_single_json_status($notice) + { + $this->initDocument('json'); + $status = $this->twitterStatusArray($notice); + $this->showJsonObjects($status); + $this->endDocument('json'); + } + + function showXmlTimeline($notice) + { + $this->initDocument('xml'); + $this->elementStart('statuses', array('type' => 'array', + 'xmlns:statusnet' => 'http://status.net/schema/api/1/')); + + if (is_array($notice)) { + $notice = new ArrayWrapper($notice); + } + + while ($notice->fetch()) { + try { + $twitter_status = $this->twitterStatusArray($notice); + $this->showTwitterXmlStatus($twitter_status); + } catch (Exception $e) { + common_log(LOG_ERR, $e->getMessage()); + continue; + } + } + + $this->elementEnd('statuses'); + $this->endDocument('xml'); + } + + function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null) + { + $this->initDocument('rss'); + + $this->element('title', null, $title); + $this->element('link', null, $link); + + if (!is_null($self)) { + $this->element( + 'atom:link', + array( + 'type' => 'application/rss+xml', + 'href' => $self, + 'rel' => 'self' + ) + ); + } + + if (!is_null($suplink)) { + // For FriendFeed's SUP protocol + $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom', + 'rel' => 'http://api.friendfeed.com/2008/03#sup', + 'href' => $suplink, + 'type' => 'application/json')); + } + + if (!is_null($logo)) { + $this->elementStart('image'); + $this->element('link', null, $link); + $this->element('title', null, $title); + $this->element('url', null, $logo); + $this->elementEnd('image'); + } + + $this->element('description', null, $subtitle); + $this->element('language', null, 'en-us'); + $this->element('ttl', null, '40'); + + if (is_array($notice)) { + $notice = new ArrayWrapper($notice); + } + + while ($notice->fetch()) { + try { + $entry = $this->twitterRssEntryArray($notice); + $this->showTwitterRssItem($entry); + } catch (Exception $e) { + common_log(LOG_ERR, $e->getMessage()); + // continue on exceptions + } + } + + $this->endTwitterRss(); + } + + function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null) + { + $this->initDocument('atom'); + + $this->element('title', null, $title); + $this->element('id', null, $id); + $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null); + + if (!is_null($logo)) { + $this->element('logo',null,$logo); + } + + if (!is_null($suplink)) { + // For FriendFeed's SUP protocol + $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup', + 'href' => $suplink, + 'type' => 'application/json')); + } + + if (!is_null($selfuri)) { + $this->element('link', array('href' => $selfuri, + 'rel' => 'self', 'type' => 'application/atom+xml'), null); + } + + $this->element('updated', null, common_date_iso8601('now')); + $this->element('subtitle', null, $subtitle); + + if (is_array($notice)) { + $notice = new ArrayWrapper($notice); + } + + while ($notice->fetch()) { + try { + $this->raw($notice->asAtomEntry()); + } catch (Exception $e) { + common_log(LOG_ERR, $e->getMessage()); + continue; + } + } + + $this->endDocument('atom'); + } + + function showRssGroups($group, $title, $link, $subtitle) + { + $this->initDocument('rss'); + + $this->element('title', null, $title); + $this->element('link', null, $link); + $this->element('description', null, $subtitle); + $this->element('language', null, 'en-us'); + $this->element('ttl', null, '40'); + + if (is_array($group)) { + foreach ($group as $g) { + $twitter_group = $this->twitterRssGroupArray($g); + $this->showTwitterRssItem($twitter_group); + } + } else { + while ($group->fetch()) { + $twitter_group = $this->twitterRssGroupArray($group); + $this->showTwitterRssItem($twitter_group); + } + } + + $this->endTwitterRss(); + } + + function showTwitterAtomEntry($entry) + { + $this->elementStart('entry'); + $this->element('title', null, common_xml_safe_str($entry['title'])); + $this->element( + 'content', + array('type' => 'html'), + common_xml_safe_str($entry['content']) + ); + $this->element('id', null, $entry['id']); + $this->element('published', null, $entry['published']); + $this->element('updated', null, $entry['updated']); + $this->element('link', array('type' => 'text/html', + 'href' => $entry['link'], + 'rel' => 'alternate')); + $this->element('link', array('type' => $entry['avatar-type'], + 'href' => $entry['avatar'], + 'rel' => 'image')); + $this->elementStart('author'); + + $this->element('name', null, $entry['author-name']); + $this->element('uri', null, $entry['author-uri']); + + $this->elementEnd('author'); + $this->elementEnd('entry'); + } + + function showXmlDirectMessage($dm, $namespaces=false) + { + $attrs = array(); + if ($namespaces) { + $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/'; + } + $this->elementStart('direct_message', $attrs); + foreach($dm as $element => $value) { + switch ($element) { + case 'sender': + case 'recipient': + $this->showTwitterXmlUser($value, $element); + break; + case 'text': + $this->element($element, null, common_xml_safe_str($value)); + break; + default: + $this->element($element, null, $value); + break; + } + } + $this->elementEnd('direct_message'); + } + + function directMessageArray($message) + { + $dmsg = array(); + + $from_profile = $message->getFrom(); + $to_profile = $message->getTo(); + + $dmsg['id'] = intval($message->id); + $dmsg['sender_id'] = intval($from_profile->id); + $dmsg['text'] = trim($message->content); + $dmsg['recipient_id'] = intval($to_profile->id); + $dmsg['created_at'] = $this->dateTwitter($message->created); + $dmsg['sender_screen_name'] = $from_profile->nickname; + $dmsg['recipient_screen_name'] = $to_profile->nickname; + $dmsg['sender'] = $this->twitterUserArray($from_profile, false); + $dmsg['recipient'] = $this->twitterUserArray($to_profile, false); + + return $dmsg; + } + + function rssDirectMessageArray($message) + { + $entry = array(); + + $from = $message->getFrom(); + + $entry['title'] = sprintf('Message from %1$s to %2$s', + $from->nickname, $message->getTo()->nickname); + + $entry['content'] = common_xml_safe_str($message->rendered); + $entry['link'] = common_local_url('showmessage', array('message' => $message->id)); + $entry['published'] = common_date_iso8601($message->created); + + $taguribase = TagURI::base(); + + $entry['id'] = "tag:$taguribase:$entry[link]"; + $entry['updated'] = $entry['published']; + + $entry['author-name'] = $from->getBestName(); + $entry['author-uri'] = $from->homepage; + + $avatar = $from->getAvatar(AVATAR_STREAM_SIZE); + + $entry['avatar'] = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE); + $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png'; + + // RSS item specific + + $entry['description'] = $entry['content']; + $entry['pubDate'] = common_date_rfc2822($message->created); + $entry['guid'] = $entry['link']; + + return $entry; + } + + function showSingleXmlDirectMessage($message) + { + $this->initDocument('xml'); + $dmsg = $this->directMessageArray($message); + $this->showXmlDirectMessage($dmsg, true); + $this->endDocument('xml'); + } + + function showSingleJsonDirectMessage($message) + { + $this->initDocument('json'); + $dmsg = $this->directMessageArray($message); + $this->showJsonObjects($dmsg); + $this->endDocument('json'); + } + + function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null) + { + $this->initDocument('atom'); + + $this->element('title', null, common_xml_safe_str($title)); + $this->element('id', null, $id); + $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null); + + if (!is_null($selfuri)) { + $this->element('link', array('href' => $selfuri, + 'rel' => 'self', 'type' => 'application/atom+xml'), null); + } + + $this->element('updated', null, common_date_iso8601('now')); + $this->element('subtitle', null, common_xml_safe_str($subtitle)); + + if (is_array($group)) { + foreach ($group as $g) { + $this->raw($g->asAtomEntry()); + } + } else { + while ($group->fetch()) { + $this->raw($group->asAtomEntry()); + } + } + + $this->endDocument('atom'); + + } + + function showJsonTimeline($notice) + { + $this->initDocument('json'); + + $statuses = array(); + + if (is_array($notice)) { + $notice = new ArrayWrapper($notice); + } + + while ($notice->fetch()) { + try { + $twitter_status = $this->twitterStatusArray($notice); + array_push($statuses, $twitter_status); + } catch (Exception $e) { + common_log(LOG_ERR, $e->getMessage()); + continue; + } + } + + $this->showJsonObjects($statuses); + + $this->endDocument('json'); + } + + function showJsonGroups($group) + { + $this->initDocument('json'); + + $groups = array(); + + if (is_array($group)) { + foreach ($group as $g) { + $twitter_group = $this->twitterGroupArray($g); + array_push($groups, $twitter_group); + } + } else { + while ($group->fetch()) { + $twitter_group = $this->twitterGroupArray($group); + array_push($groups, $twitter_group); + } + } + + $this->showJsonObjects($groups); + + $this->endDocument('json'); + } + + function showXmlGroups($group) + { + + $this->initDocument('xml'); + $this->elementStart('groups', array('type' => 'array')); + + if (is_array($group)) { + foreach ($group as $g) { + $twitter_group = $this->twitterGroupArray($g); + $this->showTwitterXmlGroup($twitter_group); + } + } else { + while ($group->fetch()) { + $twitter_group = $this->twitterGroupArray($group); + $this->showTwitterXmlGroup($twitter_group); + } + } + + $this->elementEnd('groups'); + $this->endDocument('xml'); + } + + function showXmlLists($list, $next_cursor=0, $prev_cursor=0) + { + + $this->initDocument('xml'); + $this->elementStart('lists_list'); + $this->elementStart('lists', array('type' => 'array')); + + if (is_array($list)) { + foreach ($list as $l) { + $twitter_list = $this->twitterListArray($l); + $this->showTwitterXmlList($twitter_list); + } + } else { + while ($list->fetch()) { + $twitter_list = $this->twitterListArray($list); + $this->showTwitterXmlList($twitter_list); + } + } + + $this->elementEnd('lists'); + + $this->element('next_cursor', null, $next_cursor); + $this->element('previous_cursor', null, $prev_cursor); + + $this->elementEnd('lists_list'); + $this->endDocument('xml'); + } + + function showJsonLists($list, $next_cursor=0, $prev_cursor=0) + { + $this->initDocument('json'); + + $lists = array(); + + if (is_array($list)) { + foreach ($list as $l) { + $twitter_list = $this->twitterListArray($l); + array_push($lists, $twitter_list); + } + } else { + while ($list->fetch()) { + $twitter_list = $this->twitterListArray($list); + array_push($lists, $twitter_list); + } + } + + $lists_list = array( + 'lists' => $lists, + 'next_cursor' => $next_cursor, + 'next_cursor_str' => strval($next_cursor), + 'previous_cursor' => $prev_cursor, + 'previous_cursor_str' => strval($prev_cursor) + ); + + $this->showJsonObjects($lists_list); + + $this->endDocument('json'); + } + + function showTwitterXmlUsers($user) + { + $this->initDocument('xml'); + $this->elementStart('users', array('type' => 'array', + 'xmlns:statusnet' => 'http://status.net/schema/api/1/')); + + if (is_array($user)) { + foreach ($user as $u) { + $twitter_user = $this->twitterUserArray($u); + $this->showTwitterXmlUser($twitter_user); + } + } else { + while ($user->fetch()) { + $twitter_user = $this->twitterUserArray($user); + $this->showTwitterXmlUser($twitter_user); + } + } + + $this->elementEnd('users'); + $this->endDocument('xml'); + } + + function showJsonUsers($user) + { + $this->initDocument('json'); + + $users = array(); + + if (is_array($user)) { + foreach ($user as $u) { + $twitter_user = $this->twitterUserArray($u); + array_push($users, $twitter_user); + } + } else { + while ($user->fetch()) { + $twitter_user = $this->twitterUserArray($user); + array_push($users, $twitter_user); + } + } + + $this->showJsonObjects($users); + + $this->endDocument('json'); + } + + function showSingleJsonGroup($group) + { + $this->initDocument('json'); + $twitter_group = $this->twitterGroupArray($group); + $this->showJsonObjects($twitter_group); + $this->endDocument('json'); + } + + function showSingleXmlGroup($group) + { + $this->initDocument('xml'); + $twitter_group = $this->twitterGroupArray($group); + $this->showTwitterXmlGroup($twitter_group); + $this->endDocument('xml'); + } + + function showSingleJsonList($list) + { + $this->initDocument('json'); + $twitter_list = $this->twitterListArray($list); + $this->showJsonObjects($twitter_list); + $this->endDocument('json'); + } + + function showSingleXmlList($list) + { + $this->initDocument('xml'); + $twitter_list = $this->twitterListArray($list); + $this->showTwitterXmlList($twitter_list); + $this->endDocument('xml'); + } + + function dateTwitter($dt) + { + $dateStr = date('d F Y H:i:s', strtotime($dt)); + $d = new DateTime($dateStr, new DateTimeZone('UTC')); + $d->setTimezone(new DateTimeZone(common_timezone())); + return $d->format('D M d H:i:s O Y'); + } + + function initDocument($type='xml') + { + switch ($type) { + case 'xml': + header('Content-Type: application/xml; charset=utf-8'); + $this->startXML(); + break; + case 'json': + header('Content-Type: application/json; charset=utf-8'); + + // Check for JSONP callback + if (isset($this->callback)) { + print $this->callback . '('; + } + break; + case 'rss': + header("Content-Type: application/rss+xml; charset=utf-8"); + $this->initTwitterRss(); + break; + case 'atom': + header('Content-Type: application/atom+xml; charset=utf-8'); + $this->initTwitterAtom(); + break; + default: + // TRANS: Client error on an API request with an unsupported data format. + $this->clientError(_('Not a supported data format.')); + break; + } + + return; + } + + function endDocument($type='xml') + { + switch ($type) { + case 'xml': + $this->endXML(); + break; + case 'json': + // Check for JSONP callback + if (isset($this->callback)) { + print ')'; + } + break; + case 'rss': + $this->endTwitterRss(); + break; + case 'atom': + $this->endTwitterRss(); + break; + default: + // TRANS: Client error on an API request with an unsupported data format. + $this->clientError(_('Not a supported data format.')); + break; + } + return; + } + + function clientError($msg, $code = 400, $format = null) + { + $action = $this->trimmed('action'); + if ($format === null) { + $format = $this->format; + } + + common_debug("User error '$code' on '$action': $msg", __FILE__); + + if (!array_key_exists($code, ClientErrorAction::$status)) { + $code = 400; + } + + $status_string = ClientErrorAction::$status[$code]; + + // Do not emit error header for JSONP + if (!isset($this->callback)) { + header('HTTP/1.1 ' . $code . ' ' . $status_string); + } + + switch($format) { + case 'xml': + $this->initDocument('xml'); + $this->elementStart('hash'); + $this->element('error', null, $msg); + $this->element('request', null, $_SERVER['REQUEST_URI']); + $this->elementEnd('hash'); + $this->endDocument('xml'); + break; + case 'json': + $this->initDocument('json'); + $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']); + print(json_encode($error_array)); + $this->endDocument('json'); + break; + case 'text': + header('Content-Type: text/plain; charset=utf-8'); + print $msg; + break; + default: + // If user didn't request a useful format, throw a regular client error + throw new ClientException($msg, $code); + } + } + + function serverError($msg, $code = 500, $content_type = null) + { + $action = $this->trimmed('action'); + if ($content_type === null) { + $content_type = $this->format; + } + + common_debug("Server error '$code' on '$action': $msg", __FILE__); + + if (!array_key_exists($code, ServerErrorAction::$status)) { + $code = 400; + } + + $status_string = ServerErrorAction::$status[$code]; + + // Do not emit error header for JSONP + if (!isset($this->callback)) { + header('HTTP/1.1 '.$code.' '.$status_string); + } + + if ($content_type == 'xml') { + $this->initDocument('xml'); + $this->elementStart('hash'); + $this->element('error', null, $msg); + $this->element('request', null, $_SERVER['REQUEST_URI']); + $this->elementEnd('hash'); + $this->endDocument('xml'); + } else { + $this->initDocument('json'); + $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']); + print(json_encode($error_array)); + $this->endDocument('json'); + } + } + + function initTwitterRss() + { + $this->startXML(); + $this->elementStart( + 'rss', + array( + 'version' => '2.0', + 'xmlns:atom' => 'http://www.w3.org/2005/Atom', + 'xmlns:georss' => 'http://www.georss.org/georss' + ) + ); + $this->elementStart('channel'); + Event::handle('StartApiRss', array($this)); + } + + function endTwitterRss() + { + $this->elementEnd('channel'); + $this->elementEnd('rss'); + $this->endXML(); + } + + function initTwitterAtom() + { + $this->startXML(); + // FIXME: don't hardcode the language here! + $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom', + 'xml:lang' => 'en-US', + 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0')); + } + + function endTwitterAtom() + { + $this->elementEnd('feed'); + $this->endXML(); + } + + function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true) + { + $profile_array = $this->twitterUserArray($profile, $includeStatuses); + switch ($content_type) { + case 'xml': + $this->showTwitterXmlUser($profile_array); + break; + case 'json': + $this->showJsonObjects($profile_array); + break; + default: + // TRANS: Client error on an API request with an unsupported data format. + $this->clientError(_('Not a supported data format.')); + return; + } + return; + } + + private static function is_decimal($str) + { + return preg_match('/^[0-9]+$/', $str); + } + + function getTargetUser($id) + { + if (empty($id)) { + // Twitter supports these other ways of passing the user ID + if (self::is_decimal($this->arg('id'))) { + return User::staticGet($this->arg('id')); + } else if ($this->arg('id')) { + $nickname = common_canonical_nickname($this->arg('id')); + return User::staticGet('nickname', $nickname); + } else if ($this->arg('user_id')) { + // This is to ensure that a non-numeric user_id still + // overrides screen_name even if it doesn't get used + if (self::is_decimal($this->arg('user_id'))) { + return User::staticGet('id', $this->arg('user_id')); + } + } else if ($this->arg('screen_name')) { + $nickname = common_canonical_nickname($this->arg('screen_name')); + return User::staticGet('nickname', $nickname); + } else { + // Fall back to trying the currently authenticated user + return $this->auth_user; + } + + } else if (self::is_decimal($id)) { + return User::staticGet($id); + } else { + $nickname = common_canonical_nickname($id); + return User::staticGet('nickname', $nickname); + } + } + + function getTargetProfile($id) + { + if (empty($id)) { + + // Twitter supports these other ways of passing the user ID + if (self::is_decimal($this->arg('id'))) { + return Profile::staticGet($this->arg('id')); + } else if ($this->arg('id')) { + // Screen names currently can only uniquely identify a local user. + $nickname = common_canonical_nickname($this->arg('id')); + $user = User::staticGet('nickname', $nickname); + return $user ? $user->getProfile() : null; + } else if ($this->arg('user_id')) { + // This is to ensure that a non-numeric user_id still + // overrides screen_name even if it doesn't get used + if (self::is_decimal($this->arg('user_id'))) { + return Profile::staticGet('id', $this->arg('user_id')); + } + } else if ($this->arg('screen_name')) { + $nickname = common_canonical_nickname($this->arg('screen_name')); + $user = User::staticGet('nickname', $nickname); + return $user ? $user->getProfile() : null; + } + } else if (self::is_decimal($id)) { + return Profile::staticGet($id); + } else { + $nickname = common_canonical_nickname($id); + $user = User::staticGet('nickname', $nickname); + return $user ? $user->getProfile() : null; + } + } + + function getTargetGroup($id) + { + if (empty($id)) { + if (self::is_decimal($this->arg('id'))) { + return User_group::staticGet('id', $this->arg('id')); + } else if ($this->arg('id')) { + return User_group::getForNickname($this->arg('id')); + } else if ($this->arg('group_id')) { + // This is to ensure that a non-numeric group_id still + // overrides group_name even if it doesn't get used + if (self::is_decimal($this->arg('group_id'))) { + return User_group::staticGet('id', $this->arg('group_id')); + } + } else if ($this->arg('group_name')) { + return User_group::getForNickname($this->arg('group_name')); + } + + } else if (self::is_decimal($id)) { + return User_group::staticGet('id', $id); + } else if ($this->arg('uri')) { + return User_group::staticGet('uri', urldecode($this->arg('uri'))); + } else { + return User_group::getForNickname($id); + } + } + + function getTargetList($user=null, $id=null) + { + $tagger = $this->getTargetUser($user); + $list = null; + + if (empty($id)) { + $id = $this->arg('id'); + } + + if($id) { + if (is_numeric($id)) { + $list = Profile_list::staticGet('id', $id); + + // only if the list with the id belongs to the tagger + if(empty($list) || $list->tagger != $tagger->id) { + $list = null; + } + } + if (empty($list)) { + $tag = common_canonical_tag($id); + $list = Profile_list::getByTaggerAndTag($tagger->id, $tag); + } + + if (!empty($list) && $list->private) { + if ($this->auth_user->id == $list->tagger) { + return $list; + } + } else { + return $list; + } + } + return null; + } + + /** + * Returns query argument or default value if not found. Certain + * parameters used throughout the API are lightly scrubbed and + * bounds checked. This overrides Action::arg(). + * + * @param string $key requested argument + * @param string $def default value to return if $key is not provided + * + * @return var $var + */ + function arg($key, $def=null) + { + // XXX: Do even more input validation/scrubbing? + + if (array_key_exists($key, $this->args)) { + switch($key) { + case 'page': + $page = (int)$this->args['page']; + return ($page < 1) ? 1 : $page; + case 'count': + $count = (int)$this->args['count']; + if ($count < 1) { + return 20; + } elseif ($count > 200) { + return 200; + } else { + return $count; + } + case 'since_id': + $since_id = (int)$this->args['since_id']; + return ($since_id < 1) ? 0 : $since_id; + case 'max_id': + $max_id = (int)$this->args['max_id']; + return ($max_id < 1) ? 0 : $max_id; + default: + return parent::arg($key, $def); + } + } else { + return $def; + } + } + + /** + * Calculate the complete URI that called up this action. Used for + * Atom rel="self" links. Warning: this is funky. + * + * @return string URL a URL suitable for rel="self" Atom links + */ + function getSelfUri() + { + $action = mb_substr(get_class($this), 0, -6); // remove 'Action' + + $id = $this->arg('id'); + $aargs = array('format' => $this->format); + if (!empty($id)) { + $aargs['id'] = $id; + } + + $tag = $this->arg('tag'); + if (!empty($tag)) { + $aargs['tag'] = $tag; + } + + parse_str($_SERVER['QUERY_STRING'], $params); + $pstring = ''; + if (!empty($params)) { + unset($params['p']); + $pstring = http_build_query($params); + } + + $uri = common_local_url($action, $aargs); + + if (!empty($pstring)) { + $uri .= '?' . $pstring; + } + + return $uri; + } +} diff --git a/api-changes-1.1.1/lib/router.php b/api-changes-1.1.1/lib/router.php new file mode 100644 index 0000000..cbed72a --- /dev/null +++ b/api-changes-1.1.1/lib/router.php @@ -0,0 +1,1193 @@ +. + * + * @category URL + * @package StatusNet + * @author Evan Prodromou + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * URL Router + * + * Cheap wrapper around Net_URL_Mapper + * + * @category URL + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class Router +{ + var $m = null; + static $inst = null; + + const REGEX_TAG = '[^\/]+'; // [\pL\pN_\-\.]{1,64} better if we can do unicode regexes + + static function get() + { + if (!Router::$inst) { + Router::$inst = new Router(); + } + return Router::$inst; + } + + /** + * Clear the global singleton instance for this class. + * Needed to ensure reset when switching site configurations. + */ + static function clear() + { + Router::$inst = null; + } + + function __construct() + { + if (empty($this->m)) { + $this->m = $this->initialize(); + } + } + + /** + * Create a unique hashkey for the router. + * + * The router's url map can change based on the version of the software + * you're running and the plugins that are enabled. To avoid having bad routes + * get stuck in the cache, the key includes a list of plugins and the software + * version. + * + * There can still be problems with a) differences in versions of the plugins and + * b) people running code between official versions, but these tend to be more + * sophisticated users who can grok what's going on and clear their caches. + * + * @return string cache key string that should uniquely identify a router + */ + + static function cacheKey() + { + $parts = array('router'); + + // Many router paths depend on this setting. + if (common_config('singleuser', 'enabled')) { + $parts[] = '1user'; + } else { + $parts[] = 'multi'; + } + + return Cache::codeKey(implode(':', $parts)); + } + + function initialize() + { + $m = new URLMapper(); + + if (Event::handle('StartInitializeRouter', array(&$m))) { + + $m->connect('robots.txt', array('action' => 'robotstxt')); + + $m->connect('opensearch/people', array('action' => 'opensearch', + 'type' => 'people')); + $m->connect('opensearch/notice', array('action' => 'opensearch', + 'type' => 'notice')); + + // docs + + $m->connect('doc/:title', array('action' => 'doc')); + + $m->connect('main/otp/:user_id/:token', + array('action' => 'otp'), + array('user_id' => '[0-9]+', + 'token' => '.+')); + + // these take a code; before the main part + + foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) { + $m->connect('main/'.$c.'/:code', array('action' => $c)); + } + + // Also need a block variant accepting ID on URL for mail links + $m->connect('main/block/:profileid', + array('action' => 'block'), + array('profileid' => '[0-9]+')); + + $m->connect('main/sup/:seconds', array('action' => 'sup'), + array('seconds' => '[0-9]+')); + + // main stuff is repetitive + + $main = array('login', 'logout', 'register', 'subscribe', + 'unsubscribe', 'cancelsubscription', 'approvesub', + 'confirmaddress', 'recoverpassword', + 'invite', 'favor', 'disfavor', 'sup', + 'block', 'unblock', 'subedit', + 'groupblock', 'groupunblock', + 'sandbox', 'unsandbox', + 'silence', 'unsilence', + 'grantrole', 'revokerole', + 'repeat', + 'deleteuser', + 'geocode', + 'version', + 'backupaccount', + 'deleteaccount', + 'restoreaccount', + 'top', + ); + + foreach ($main as $a) { + $m->connect('main/'.$a, array('action' => $a)); + } + + $m->connect('main/tagprofile/:id', array('action' => 'tagprofile'), + array('id' => '[0-9]+')); + + $m->connect('main/tagprofile', array('action' => 'tagprofile')); + + $m->connect('main/oembed', + array('action' => 'oembed')); + + $m->connect('main/xrds', + array('action' => 'publicxrds')); + $m->connect('.well-known/host-meta', + array('action' => 'hostmeta')); + $m->connect('main/xrd', + array('action' => 'userxrd')); + + // settings + + foreach (array('profile', 'avatar', 'password', 'im', 'oauthconnections', + 'oauthapps', 'email', 'sms', 'url') as $s) { + $m->connect('settings/'.$s, array('action' => $s.'settings')); + } + + if (common_config('oldschool', 'enabled')) { + $m->connect('settings/oldschool', array('action' => 'oldschoolsettings')); + } + + $m->connect('settings/oauthapps/show/:id', + array('action' => 'showapplication'), + array('id' => '[0-9]+') + ); + $m->connect('settings/oauthapps/new', + array('action' => 'newapplication') + ); + $m->connect('settings/oauthapps/edit/:id', + array('action' => 'editapplication'), + array('id' => '[0-9]+') + ); + $m->connect('settings/oauthapps/delete/:id', + array('action' => 'deleteapplication'), + array('id' => '[0-9]+') + ); + + // search + + foreach (array('group', 'people', 'notice') as $s) { + $m->connect('search/'.$s.'?q=:q', + array('action' => $s.'search'), + array('q' => '.+')); + $m->connect('search/'.$s, array('action' => $s.'search')); + } + + // The second of these is needed to make the link work correctly + // when inserted into the page. The first is needed to match the + // route on the way in. Seems to be another Net_URL_Mapper bug to me. + $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'), + array('q' => '.+')); + $m->connect('search/notice/rss', array('action' => 'noticesearchrss')); + + $m->connect('attachment/:attachment', + array('action' => 'attachment'), + array('attachment' => '[0-9]+')); + + $m->connect('attachment/:attachment/ajax', + array('action' => 'attachment_ajax'), + array('attachment' => '[0-9]+')); + + $m->connect('attachment/:attachment/thumbnail', + array('action' => 'attachment_thumbnail'), + array('attachment' => '[0-9]+')); + + $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto', + array('action' => 'newnotice'), + array('replyto' => Nickname::DISPLAY_FMT), + array('inreplyto' => '[0-9]+')); + + $m->connect('notice/new?replyto=:replyto', + array('action' => 'newnotice'), + array('replyto' => Nickname::DISPLAY_FMT)); + + $m->connect('notice/new', array('action' => 'newnotice')); + + $m->connect('notice/:notice/file', + array('action' => 'file'), + array('notice' => '[0-9]+')); + + $m->connect('notice/:notice', + array('action' => 'shownotice'), + array('notice' => '[0-9]+')); + + $m->connect('notice/delete/:notice', + array('action' => 'deletenotice'), + array('notice' => '[0-9]+')); + + $m->connect('notice/delete', array('action' => 'deletenotice')); + + $m->connect('bookmarklet/new', array('action' => 'bookmarklet')); + + // conversation + + $m->connect('conversation/:id', + array('action' => 'conversation'), + array('id' => '[0-9]+')); + $m->connect('conversation/:id/replies', + array('action' => 'conversationreplies'), + array('id' => '[0-9]+')); + + $m->connect('message/new', array('action' => 'newmessage')); + $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => Nickname::DISPLAY_FMT)); + $m->connect('message/:message', + array('action' => 'showmessage'), + array('message' => '[0-9]+')); + + $m->connect('user/:id', + array('action' => 'userbyid'), + array('id' => '[0-9]+')); + + if (!common_config('performance', 'high')) { + $m->connect('tags/', array('action' => 'publictagcloud')); + $m->connect('tag/', array('action' => 'publictagcloud')); + $m->connect('tags', array('action' => 'publictagcloud')); + $m->connect('tag', array('action' => 'publictagcloud')); + } + $m->connect('tag/:tag/rss', + array('action' => 'tagrss'), + array('tag' => self::REGEX_TAG)); + $m->connect('tag/:tag', + array('action' => 'tag'), + array('tag' => self::REGEX_TAG)); + + // groups + + $m->connect('group/new', array('action' => 'newgroup')); + + foreach (array('edit', 'join', 'leave', 'delete', 'cancel', 'approve') as $v) { + $m->connect('group/:nickname/'.$v, + array('action' => $v.'group'), + array('nickname' => Nickname::DISPLAY_FMT)); + $m->connect('group/:id/id/'.$v, + array('action' => $v.'group'), + array('id' => '[0-9]+')); + } + + foreach (array('members', 'logo', 'rss') as $n) { + $m->connect('group/:nickname/'.$n, + array('action' => 'group'.$n), + array('nickname' => Nickname::DISPLAY_FMT)); + } + + $m->connect('group/:nickname/foaf', + array('action' => 'foafgroup'), + array('nickname' => Nickname::DISPLAY_FMT)); + + $m->connect('group/:nickname/blocked', + array('action' => 'blockedfromgroup'), + array('nickname' => Nickname::DISPLAY_FMT)); + + $m->connect('group/:nickname/makeadmin', + array('action' => 'makeadmin'), + array('nickname' => Nickname::DISPLAY_FMT)); + + $m->connect('group/:nickname/members/pending', + array('action' => 'groupqueue'), + array('nickname' => Nickname::DISPLAY_FMT)); + + $m->connect('group/:id/id', + array('action' => 'groupbyid'), + array('id' => '[0-9]+')); + + $m->connect('group/:nickname', + array('action' => 'showgroup'), + array('nickname' => Nickname::DISPLAY_FMT)); + + $m->connect('group/:nickname/', + array('action' => 'showgroup'), + array('nickname' => Nickname::DISPLAY_FMT)); + + $m->connect('group/', array('action' => 'groups')); + $m->connect('group', array('action' => 'groups')); + $m->connect('groups/', array('action' => 'groups')); + $m->connect('groups', array('action' => 'groups')); + + // Twitter-compatible API + + // statuses API + + $m->connect('api', + array('action' => 'Redirect', + 'nextAction' => 'doc', + 'args' => array('title' => 'api'))); + + $m->connect('api/statuses/public_timeline.:format', + array('action' => 'ApiTimelinePublic', + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/friends_timeline/:id.:format', + array('action' => 'ApiTimelineFriends', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/friends_timeline.:format', + array('action' => 'ApiTimelineFriends', + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/home_timeline/:id.:format', + array('action' => 'ApiTimelineHome', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/home_timeline.:format', + array('action' => 'ApiTimelineHome', + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/user_timeline/:id.:format', + array('action' => 'ApiTimelineUser', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/user_timeline.:format', + array('action' => 'ApiTimelineUser', + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/mentions/:id.:format', + array('action' => 'ApiTimelineMentions', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/mentions.:format', + array('action' => 'ApiTimelineMentions', + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/replies/:id.:format', + array('action' => 'ApiTimelineMentions', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/replies.:format', + array('action' => 'ApiTimelineMentions', + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statuses/retweeted_by_me.:format', + array('action' => 'ApiTimelineRetweetedByMe', + 'format' => '(xml|json|atom|as)')); + + $m->connect('api/statuses/retweeted_to_me.:format', + array('action' => 'ApiTimelineRetweetedToMe', + 'format' => '(xml|json|atom|as)')); + + $m->connect('api/statuses/retweets_of_me.:format', + array('action' => 'ApiTimelineRetweetsOfMe', + 'format' => '(xml|json|atom|as)')); + + $m->connect('api/statuses/friends/:id.:format', + array('action' => 'ApiUserFriends', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/statuses/friends.:format', + array('action' => 'ApiUserFriends', + 'format' => '(xml|json)')); + + $m->connect('api/statuses/followers/:id.:format', + array('action' => 'ApiUserFollowers', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/statuses/followers.:format', + array('action' => 'ApiUserFollowers', + 'format' => '(xml|json)')); + + $m->connect('api/statuses/show/:id.:format', + array('action' => 'ApiStatusesShow', + 'id' => '[0-9]+', + 'format' => '(xml|json|atom)')); + + $m->connect('api/statuses/show.:format', + array('action' => 'ApiStatusesShow', + 'format' => '(xml|json|atom)')); + + $m->connect('api/statuses/update.:format', + array('action' => 'ApiStatusesUpdate', + 'format' => '(xml|json)')); + + $m->connect('api/statuses/destroy/:id.:format', + array('action' => 'ApiStatusesDestroy', + 'id' => '[0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/statuses/destroy.:format', + array('action' => 'ApiStatusesDestroy', + 'format' => '(xml|json)')); + + $m->connect('api/statuses/retweet/:id.:format', + array('action' => 'ApiStatusesRetweet', + 'id' => '[0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/statuses/retweets/:id.:format', + array('action' => 'ApiStatusesRetweets', + 'id' => '[0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/statuses/favs/:id.json', + array('action' => 'ApiStatusesFavs', + 'id' => '[0-9]+')); + + $m->connect('api/attachment/:id.json', + array('action' => 'ApiAttachment', + 'id' => '[0-9]+')); + + $m->connect('api/checkhub.json', + array('action' => 'ApiCheckHub')); + + $m->connect('api/externalprofile/show.json', + array('action' => 'ApiExternalProfileShow')); + + // users + + $m->connect('api/users/show/:id.:format', + array('action' => 'ApiUserShow', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/users/show.:format', + array('action' => 'ApiUserShow', + 'format' => '(xml|json)')); + + $m->connect('api/users/profile_image/:screen_name.:format', + array('action' => 'ApiUserProfileImage', + 'screen_name' => Nickname::DISPLAY_FMT, + 'format' => '(xml|json)')); + + // direct messages + + $m->connect('api/direct_messages.:format', + array('action' => 'ApiDirectMessage', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/direct_messages/sent.:format', + array('action' => 'ApiDirectMessage', + 'format' => '(xml|json|rss|atom)', + 'sent' => true)); + + $m->connect('api/direct_messages/new.:format', + array('action' => 'ApiDirectMessageNew', + 'format' => '(xml|json)')); + + // friendships + + $m->connect('api/friendships/show.:format', + array('action' => 'ApiFriendshipsShow', + 'format' => '(xml|json)')); + + $m->connect('api/friendships/exists.:format', + array('action' => 'ApiFriendshipsExists', + 'format' => '(xml|json)')); + + $m->connect('api/friendships/create/:id.:format', + array('action' => 'ApiFriendshipsCreate', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/friendships/create.:format', + array('action' => 'ApiFriendshipsCreate', + 'format' => '(xml|json)')); + + $m->connect('api/friendships/destroy/:id.:format', + array('action' => 'ApiFriendshipsDestroy', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/friendships/destroy.:format', + array('action' => 'ApiFriendshipsDestroy', + 'format' => '(xml|json)')); + + // Social graph + + $m->connect('api/friends/ids/:id.:format', + array('action' => 'ApiUserFriends', + 'ids_only' => true)); + + $m->connect('api/followers/ids/:id.:format', + array('action' => 'ApiUserFollowers', + 'ids_only' => true)); + + $m->connect('api/friends/ids.:format', + array('action' => 'ApiUserFriends', + 'ids_only' => true)); + + $m->connect('api/followers/ids.:format', + array('action' => 'ApiUserFollowers', + 'ids_only' => true)); + + // account + + $m->connect('api/account/verify_credentials.:format', + array('action' => 'ApiAccountVerifyCredentials')); + + $m->connect('api/account/update_profile.:format', + array('action' => 'ApiAccountUpdateProfile')); + + $m->connect('api/account/update_profile_image.:format', + array('action' => 'ApiAccountUpdateProfileImage')); + + $m->connect('api/account/update_delivery_device.:format', + array('action' => 'ApiAccountUpdateDeliveryDevice')); + + // special case where verify_credentials is called w/out a format + + $m->connect('api/account/verify_credentials', + array('action' => 'ApiAccountVerifyCredentials')); + + $m->connect('api/account/rate_limit_status.:format', + array('action' => 'ApiAccountRateLimitStatus')); + + // favorites + + $m->connect('api/favorites/:id.:format', + array('action' => 'ApiTimelineFavorites', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/favorites.:format', + array('action' => 'ApiTimelineFavorites', + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/favorites/create/:id.:format', + array('action' => 'ApiFavoriteCreate', + 'id' => '[0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/favorites/destroy/:id.:format', + array('action' => 'ApiFavoriteDestroy', + 'id' => '[0-9]+', + 'format' => '(xml|json)')); + // blocks + + $m->connect('api/blocks/create/:id.:format', + array('action' => 'ApiBlockCreate', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/blocks/create.:format', + array('action' => 'ApiBlockCreate', + 'format' => '(xml|json)')); + + $m->connect('api/blocks/destroy/:id.:format', + array('action' => 'ApiBlockDestroy', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/blocks/destroy.:format', + array('action' => 'ApiBlockDestroy', + 'format' => '(xml|json)')); + + // help + + $m->connect('api/help/test.:format', + array('action' => 'ApiHelpTest', + 'format' => '(xml|json)')); + + // statusnet + + $m->connect('api/statusnet/version.:format', + array('action' => 'ApiStatusnetVersion', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/config.:format', + array('action' => 'ApiStatusnetConfig', + 'format' => '(xml|json)')); + + // For older methods, we provide "laconica" base action + + $m->connect('api/laconica/version.:format', + array('action' => 'ApiStatusnetVersion', + 'format' => '(xml|json)')); + + $m->connect('api/laconica/config.:format', + array('action' => 'ApiStatusnetConfig', + 'format' => '(xml|json)')); + + // Groups and tags are newer than 0.8.1 so no backward-compatibility + // necessary + + // Groups + //'list' has to be handled differently, as php will not allow a method to be named 'list' + + $m->connect('api/statusnet/groups/timeline/:id.:format', + array('action' => 'ApiTimelineGroup', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json|rss|atom|as)')); + + $m->connect('api/statusnet/groups/show/:id.:format', + array('action' => 'ApiGroupShow', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/show.:format', + array('action' => 'ApiGroupShow', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/join/:id.:format', + array('action' => 'ApiGroupJoin', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/join.:format', + array('action' => 'ApiGroupJoin', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/leave/:id.:format', + array('action' => 'ApiGroupLeave', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/leave.:format', + array('action' => 'ApiGroupLeave', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/is_member.:format', + array('action' => 'ApiGroupIsMember', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/list/:id.:format', + array('action' => 'ApiGroupList', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statusnet/groups/list.:format', + array('action' => 'ApiGroupList', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statusnet/groups/list_all.:format', + array('action' => 'ApiGroupListAll', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/statusnet/groups/membership/:id.:format', + array('action' => 'ApiGroupMembership', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/membership.:format', + array('action' => 'ApiGroupMembership', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/create.:format', + array('action' => 'ApiGroupCreate', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/update/:id.:format', + array('action' => 'ApiGroupProfileUpdate', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/statusnet/conversation/:id.:format', + array('action' => 'apiconversation', + 'id' => '[0-9]+', + 'format' => '(xml|json|rss|atom|as)')); + + // Lists (people tags) + + $m->connect('api/lists/memberships.:format', + array('action' => 'ApiListMemberships', + 'format' => '(xml|json)')); + + $m->connect('api/:user/lists/memberships.:format', + array('action' => 'ApiListMemberships', + 'user' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/lists/subscriptions.:format', + array('action' => 'ApiListSubscriptions', + 'format' => '(xml|json)')); + + $m->connect('api/:user/lists/subscriptions.:format', + array('action' => 'ApiListSubscriptions', + 'user' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/lists.:format', + array('action' => 'ApiLists', + 'format' => '(xml|json)')); + + $m->connect('api/:user/lists/:id.:format', + array('action' => 'ApiList', + 'user' => '[a-zA-Z0-9]+', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/:user/lists.:format', + array('action' => 'ApiLists', + 'user' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/:user/lists/:id/statuses.:format', + array('action' => 'ApiTimelineList', + 'user' => '[a-zA-Z0-9]+', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json|rss|atom)')); + + $m->connect('api/:user/:list_id/members/:id.:format', + array('action' => 'ApiListMember', + 'user' => '[a-zA-Z0-9]+', + 'list_id' => '[a-zA-Z0-9]+', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/:user/:list_id/members.:format', + array('action' => 'ApiListMembers', + 'user' => '[a-zA-Z0-9]+', + 'list_id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/:user/:list_id/subscribers/:id.:format', + array('action' => 'ApiListSubscriber', + 'user' => '[a-zA-Z0-9]+', + 'list_id' => '[a-zA-Z0-9]+', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/:user/:list_id/subscribers.:format', + array('action' => 'ApiListSubscribers', + 'user' => '[a-zA-Z0-9]+', + 'list_id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + + // Tags + $m->connect('api/statusnet/tags/timeline/:tag.:format', + array('action' => 'ApiTimelineTag', + 'tag' => self::REGEX_TAG, + 'format' => '(xml|json|rss|atom|as)')); + + // media related + $m->connect( + 'api/statusnet/media/upload', + array('action' => 'ApiMediaUpload') + ); + + // search + $m->connect('api/search.atom', array('action' => 'ApiSearchAtom')); + $m->connect('api/search.json', array('action' => 'ApiSearchJSON')); + $m->connect('api/trends.json', array('action' => 'ApiTrends')); + + $m->connect('api/oauth/request_token', + array('action' => 'ApiOauthRequestToken')); + + $m->connect('api/oauth/access_token', + array('action' => 'ApiOauthAccessToken')); + + $m->connect('api/oauth/authorize', + array('action' => 'ApiOauthAuthorize')); + + // Admin + + $m->connect('panel/site', array('action' => 'siteadminpanel')); + $m->connect('panel/user', array('action' => 'useradminpanel')); + $m->connect('panel/access', array('action' => 'accessadminpanel')); + $m->connect('panel/paths', array('action' => 'pathsadminpanel')); + $m->connect('panel/sessions', array('action' => 'sessionsadminpanel')); + $m->connect('panel/sitenotice', array('action' => 'sitenoticeadminpanel')); + $m->connect('panel/snapshot', array('action' => 'snapshotadminpanel')); + $m->connect('panel/license', array('action' => 'licenseadminpanel')); + + $m->connect('panel/plugins', array('action' => 'pluginsadminpanel')); + $m->connect('panel/plugins/enable/:plugin', + array('action' => 'pluginenable'), + array('plugin' => '[A-Za-z0-9_]+')); + $m->connect('panel/plugins/disable/:plugin', + array('action' => 'plugindisable'), + array('plugin' => '[A-Za-z0-9_]+')); + + $m->connect('getfile/:filename', + array('action' => 'getfile'), + array('filename' => '[A-Za-z0-9._-]+')); + + // Common people-tag stuff + + $m->connect('peopletag/:tag', array('action' => 'peopletag', + 'tag' => self::REGEX_TAG)); + + $m->connect('selftag/:tag', array('action' => 'selftag', + 'tag' => self::REGEX_TAG)); + + $m->connect('main/addpeopletag', array('action' => 'addpeopletag')); + + $m->connect('main/removepeopletag', array('action' => 'removepeopletag')); + + $m->connect('main/profilecompletion', array('action' => 'profilecompletion')); + + $m->connect('main/peopletagautocomplete', array('action' => 'peopletagautocomplete')); + + // In the "root" + + if (common_config('singleuser', 'enabled')) { + + $nickname = User::singleUserNickname(); + + foreach (array('subscriptions', 'subscribers', + 'all', 'foaf', 'replies', + 'microsummary') as $a) { + $m->connect($a, + array('action' => $a, + 'nickname' => $nickname)); + } + + foreach (array('subscriptions', 'subscribers') as $a) { + $m->connect($a.'/:tag', + array('action' => $a, + 'nickname' => $nickname), + array('tag' => self::REGEX_TAG)); + } + + $m->connect('subscribers/pending', + array('action' => 'subqueue', + 'nickname' => $nickname)); + + foreach (array('rss', 'groups') as $a) { + $m->connect($a, + array('action' => 'user'.$a, + 'nickname' => $nickname)); + } + + foreach (array('all', 'replies', 'favorites') as $a) { + $m->connect($a.'/rss', + array('action' => $a.'rss', + 'nickname' => $nickname)); + } + + $m->connect('favorites', + array('action' => 'showfavorites', + 'nickname' => $nickname)); + + $m->connect('avatar/:size', + array('action' => 'avatarbynickname', + 'nickname' => $nickname), + array('size' => '(original|96|48|24)')); + + $m->connect('tag/:tag/rss', + array('action' => 'userrss', + 'nickname' => $nickname), + array('tag' => self::REGEX_TAG)); + + $m->connect('tag/:tag', + array('action' => 'showstream', + 'nickname' => $nickname), + array('tag' => self::REGEX_TAG)); + + $m->connect('rsd.xml', + array('action' => 'rsd', + 'nickname' => $nickname)); + + $m->connect('', + array('action' => 'showstream', + 'nickname' => $nickname)); + + // peopletags + + $m->connect('peopletags', + array('action' => 'peopletagsbyuser')); + + $m->connect('peopletags/private', + array('action' => 'peopletagsbyuser', + 'private' => 1)); + + $m->connect('peopletags/public', + array('action' => 'peopletagsbyuser', + 'public' => 1)); + + $m->connect('othertags', + array('action' => 'peopletagsforuser')); + + $m->connect('peopletagsubscriptions', + array('action' => 'peopletagsubscriptions')); + + $m->connect('all/:tag/subscribers', + array('action' => 'peopletagsubscribers', + 'tag' => self::REGEX_TAG)); + + $m->connect('all/:tag/tagged', + array('action' => 'peopletagged', + 'tag' => self::REGEX_TAG)); + + $m->connect('all/:tag/edit', + array('action' => 'editpeopletag', + 'tag' => self::REGEX_TAG)); + + foreach(array('subscribe', 'unsubscribe') as $v) { + $m->connect('peopletag/:id/'.$v, + array('action' => $v.'peopletag', + 'id' => '[0-9]{1,64}')); + } + $m->connect('user/:tagger_id/profiletag/:id/id', + array('action' => 'profiletagbyid', + 'tagger_id' => '[0-9]+', + 'id' => '[0-9]+')); + + $m->connect('all/:tag', + array('action' => 'showprofiletag', + 'tag' => self::REGEX_TAG)); + + foreach (array('subscriptions', 'subscribers') as $a) { + $m->connect($a.'/:tag', + array('action' => $a), + array('tag' => self::REGEX_TAG)); + } + } else { + $m->connect('', array('action' => 'public')); + $m->connect('rss', array('action' => 'publicrss')); + $m->connect('featuredrss', array('action' => 'featuredrss')); + $m->connect('favoritedrss', array('action' => 'favoritedrss')); + $m->connect('featured/', array('action' => 'featured')); + $m->connect('featured', array('action' => 'featured')); + $m->connect('favorited/', array('action' => 'favorited')); + $m->connect('favorited', array('action' => 'favorited')); + $m->connect('rsd.xml', array('action' => 'rsd')); + + foreach (array('subscriptions', 'subscribers', + 'nudge', 'all', 'foaf', 'replies', + 'inbox', 'outbox', 'microsummary') as $a) { + $m->connect(':nickname/'.$a, + array('action' => $a), + array('nickname' => Nickname::DISPLAY_FMT)); + } + $m->connect(':nickname/subscribers/pending', + array('action' => 'subqueue'), + array('nickname' => Nickname::DISPLAY_FMT)); + + // people tags + + $m->connect(':nickname/peopletags', + array('action' => 'peopletagsbyuser', + 'nickname' => Nickname::DISPLAY_FMT)); + + $m->connect(':nickname/peopletags/private', + array('action' => 'peopletagsbyuser', + 'nickname' => Nickname::DISPLAY_FMT, + 'private' => 1)); + + $m->connect(':nickname/peopletags/public', + array('action' => 'peopletagsbyuser', + 'nickname' => Nickname::DISPLAY_FMT, + 'public' => 1)); + + $m->connect(':nickname/othertags', + array('action' => 'peopletagsforuser', + 'nickname' => Nickname::DISPLAY_FMT)); + + $m->connect(':nickname/peopletagsubscriptions', + array('action' => 'peopletagsubscriptions', + 'nickname' => Nickname::DISPLAY_FMT)); + + $m->connect(':tagger/all/:tag/subscribers', + array('action' => 'peopletagsubscribers', + 'tagger' => Nickname::DISPLAY_FMT, + 'tag' => self::REGEX_TAG)); + + $m->connect(':tagger/all/:tag/tagged', + array('action' => 'peopletagged', + 'tagger' => Nickname::DISPLAY_FMT, + 'tag' => self::REGEX_TAG)); + + $m->connect(':tagger/all/:tag/edit', + array('action' => 'editpeopletag', + 'tagger' => Nickname::DISPLAY_FMT, + 'tag' => self::REGEX_TAG)); + + foreach(array('subscribe', 'unsubscribe') as $v) { + $m->connect('peopletag/:id/'.$v, + array('action' => $v.'peopletag', + 'id' => '[0-9]{1,64}')); + } + $m->connect('user/:tagger_id/profiletag/:id/id', + array('action' => 'profiletagbyid', + 'tagger_id' => '[0-9]+', + 'id' => '[0-9]+')); + + $m->connect(':tagger/all/:tag', + array('action' => 'showprofiletag', + 'tagger' => Nickname::DISPLAY_FMT, + 'tag' => self::REGEX_TAG)); + + foreach (array('subscriptions', 'subscribers') as $a) { + $m->connect(':nickname/'.$a.'/:tag', + array('action' => $a), + array('tag' => self::REGEX_TAG, + 'nickname' => Nickname::DISPLAY_FMT)); + } + + foreach (array('rss', 'groups') as $a) { + $m->connect(':nickname/'.$a, + array('action' => 'user'.$a), + array('nickname' => Nickname::DISPLAY_FMT)); + } + + foreach (array('all', 'replies', 'favorites') as $a) { + $m->connect(':nickname/'.$a.'/rss', + array('action' => $a.'rss'), + array('nickname' => Nickname::DISPLAY_FMT)); + } + + $m->connect(':nickname/favorites', + array('action' => 'showfavorites'), + array('nickname' => Nickname::DISPLAY_FMT)); + + $m->connect(':nickname/avatar/:size', + array('action' => 'avatarbynickname'), + array('size' => '(original|96|48|24)', + 'nickname' => Nickname::DISPLAY_FMT)); + + $m->connect(':nickname/tag/:tag/rss', + array('action' => 'userrss'), + array('nickname' => Nickname::DISPLAY_FMT), + array('tag' => self::REGEX_TAG)); + + $m->connect(':nickname/tag/:tag', + array('action' => 'showstream'), + array('nickname' => Nickname::DISPLAY_FMT), + array('tag' => self::REGEX_TAG)); + + $m->connect(':nickname/rsd.xml', + array('action' => 'rsd'), + array('nickname' => Nickname::DISPLAY_FMT)); + + $m->connect(':nickname', + array('action' => 'showstream'), + array('nickname' => Nickname::DISPLAY_FMT)); + + $m->connect(':nickname/', + array('action' => 'showstream'), + array('nickname' => Nickname::DISPLAY_FMT)); + } + + // AtomPub API + + $m->connect('api/statusnet/app/service/:id.xml', + array('action' => 'ApiAtomService'), + array('id' => Nickname::DISPLAY_FMT)); + + $m->connect('api/statusnet/app/service.xml', + array('action' => 'ApiAtomService')); + + $m->connect('api/statusnet/app/subscriptions/:subscriber/:subscribed.atom', + array('action' => 'AtomPubShowSubscription'), + array('subscriber' => '[0-9]+', + 'subscribed' => '[0-9]+')); + + $m->connect('api/statusnet/app/subscriptions/:subscriber.atom', + array('action' => 'AtomPubSubscriptionFeed'), + array('subscriber' => '[0-9]+')); + + $m->connect('api/statusnet/app/favorites/:profile/:notice.atom', + array('action' => 'AtomPubShowFavorite'), + array('profile' => '[0-9]+', + 'notice' => '[0-9]+')); + + $m->connect('api/statusnet/app/favorites/:profile.atom', + array('action' => 'AtomPubFavoriteFeed'), + array('profile' => '[0-9]+')); + + $m->connect('api/statusnet/app/memberships/:profile/:group.atom', + array('action' => 'AtomPubShowMembership'), + array('profile' => '[0-9]+', + 'group' => '[0-9]+')); + + $m->connect('api/statusnet/app/memberships/:profile.atom', + array('action' => 'AtomPubMembershipFeed'), + array('profile' => '[0-9]+')); + + // URL shortening + + $m->connect('url/:id', + array('action' => 'redirecturl', + 'id' => '[0-9]+')); + + // user stuff + + Event::handle('RouterInitialized', array($m)); + } + + return $m; + } + + function map($path) + { + try { + $match = $this->m->match($path); + } catch (Exception $e) { + common_log(LOG_ERR, "Problem getting route for $path - " . + $e->getMessage()); + // TRANS: Client error on action trying to visit a non-existing page. + $cac = new ClientErrorAction(_('Page not found.'), 404); + $cac->showPage(); + } + + return $match; + } + + function build($action, $args=null, $params=null, $fragment=null) + { + $action_arg = array('action' => $action); + + if ($args) { + $args = array_merge($action_arg, $args); + } else { + $args = $action_arg; + } + + $url = $this->m->generate($args, $params, $fragment); + // Due to a bug in the Net_URL_Mapper code, the returned URL may + // contain a malformed query of the form ?p1=v1?p2=v2?p3=v3. We + // repair that here rather than modifying the upstream code... + + $qpos = strpos($url, '?'); + if ($qpos !== false) { + $url = substr($url, 0, $qpos+1) . + str_replace('?', '&', substr($url, $qpos+1)); + + // @fixme this is a hacky workaround for http_build_query in the + // lower-level code and bad configs that set the default separator + // to & instead of &. Encoded &s in parameters will not be + // affected. + $url = substr($url, 0, $qpos+1) . + str_replace('&', '&', substr($url, $qpos+1)); + + } + + return $url; + } +} diff --git a/css/1.css b/css/1.css new file mode 100644 index 0000000..912e568 --- /dev/null +++ b/css/1.css @@ -0,0 +1,3088 @@ + /* · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · + · · + · · + · Q V I T T E R · + · · + · http://github.com/hannesmannerheim/qvitter · + · · + · · + · \\\\_\ · + · \\) \____) · + · · + · · + · · + · Qvitter is free software: you can redistribute it and / or modify it · + · under the terms of the GNU Affero General Public License as published by · + · the Free Software Foundation, either version three of the License or (at · + · your option) any later version. · + · · + · Qvitter is distributed in hope that it will be useful but WITHOUT ANY · + · WARRANTY; without even the implied warranty of MERCHANTABILTY or FITNESS · + · FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for · + · more details. · + · · + · You should have received a copy of the GNU Affero General Public License · + · along with Qvitter. If not, see . · + · · + · Contact h@nnesmannerhe.im if you have any questions. · + · · + · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · */ + +body { + background-color:#fff; + background-size:100% auto; + background-attachment:fixed; + margin:0; + padding:0; + padding-left:50%; + padding-top:39px; + color: #333333; + font-size: 14px; + line-height: 18px; + font-family: "Helvetica Neue",Arial,sans-serif; + } +a, a:visited, a:active { + text-decoration:none; + color:#0084B4; + } +ul, li { + margin:0; + padding:0; + list-style: none outside none; + } + +.nav-session, +.stream-item-footer .with-icn .badge-requeeted, +ul.queet-actions li .icon, +.modal-close .icon, +.dogear, +.chev-right, +.close-right, +button.icon.nav-search { + background-image: url("../img/sprite.png"); + } + +.topbar .global-nav { + background-image: url("../img/sprite_bgs.png"); + } + +#logo { + background-image:url("../img/logo.png"); + } + +.topbar { + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.25); + left: 0; + position: fixed; + right: 0; + top: 0; + z-index: 3000; + text-align:center; + } + +#search { + display:none; + height: 26px; + width: 202px; + float: right; + font-size: 12px; + margin: 7px 10px 0 7px; + position: relative; + } +#search-query { + background-color: transparent; + background-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); + color: #444444; + position: absolute; + right: 0; + top: 0; + z-index: 2; + border: 0 none; + border-radius: 13px 13px 13px 13px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset; + display: block; + font-size: 12px; + height: 14px; + line-height: 1; + padding: 6px 27px 6px 12px; + transition: all 0.2s ease-in-out 0s; + width: 163px; + margin: 0; + outline: 0 none; + font-family: "Helvetica Neue",Arial,sans-serif; + } +#search-query:focus { + box-shadow:none; + text-shadow:none; + background-color:#fff; + color:#666; + } +#search-query-hint { + background-color: #CCCCCC; + color: #999999; + position: absolute; + right: 0; + z-index: 1; + background-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); + top: 0; + border: 0 none; + border-radius: 13px 13px 13px 13px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset; + display: block; + font-size: 12px; + height: 14px; + line-height: 1; + padding: 6px 27px 6px 12px; + transition: all 0.2s ease-in-out 0s; + width: 163px; + margin: 0; + outline: 0 none; + cursor: not-allowed; + } +.search-icon { + font-size: 12px; + cursor: pointer; + display: block; + height: 26px; + position: absolute; + right: 2px; + top: 0; + width: 26px; + z-index: 3; + text-align: left; + } +button.icon.nav-search { + font-family: "Helvetica Neue",Arial,sans-serif; + border: 0 none; + background-repeat: no-repeat; + display: inline-block; + vertical-align: text-top; + height: 14px; + width: 12px; + background-position: -20px -710px; + background-color: transparent; + margin-left: 7px; + margin-top: 3px; + } +button.icon.nav-search span { + font-family: "Helvetica Neue",Arial,sans-serif; + border: 0 none; + clip: rect(0px, 0px, 0px, 0px); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + } + +.global-nav .container { + width:850px; + margin: 0 auto; + } +.language-dropdown { + float: right; + text-align: left; + margin-left: 6px; + margin-right: 0; + position: relative; + display: block; + } +.dropdown { + float: left; + position: relative; + } +.dropdown-toggle { + color: #BBBBBB; + display: block; + font-weight: bold; + height: 12px; + line-height: 1; + padding: 13px 12px 15px; + text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.75); + padding-left: 12px; + padding-right: 12px; + font-size: 12px; + position: relative; + text-decoration: none; + font-family: Arial,sans-serif; + cursor:pointer; + } +.dropdown-toggle:hover { + color:#fff; + } +.dropdown-toggle small { + font-weight: normal; + font-size: 12px; + } + +.dropdown-toggle .caret { + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #777777; + display: inline-block; + height: 0; + margin-left: 2px; + margin-top: 5px; + vertical-align: top; + width: 0; + } +.dropdown-toggle .caret b { + font-weight: normal; + } +.dropdown-toggle:hover .caret { + border-top: 4px solid #fff; + } +.dropdown-menu { + right: 0; + width: 125px; + background-clip: padding-box; + background-color: #FFFFFF; + border-color: rgba(0, 0, 0, 0.2); + border-radius: 4px 4px 4px 4px; + border-style: solid; + border-width: 1px; + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + display: none; + float: left; + margin: 2px 0 0; + padding: 4px 0; + position: absolute; + top: 100%; + z-index: 900; + } +.quitter-settings.dropdown-menu { + left:50%; + right:auto; + margin-left:-410px; + } +.quitter-settings.dropdown-menu.dropped { + display:block; + } +.dropdown-caret { + float: left; + height: 7px; + left: 12px; + overflow: hidden; + position: absolute; + top: -7px; + width: 12px; + } +.dropdown-caret .caret-outer { + white-space: nowrap; + display: inline-block; + left: 0; + margin-left: -1px; + position: absolute; + top: 0; + border-bottom: 7px solid rgba(0, 0, 0, 0.2); + border-left: 7px solid transparent; + border-right: 7px solid transparent; + } +.dropdown-caret .caret-inner { + white-space: nowrap; + margin-left: -1px; + position: absolute; + border-bottom: 6px solid #FFFFFF; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + display: inline-block; + left: 1px; + top: 1px; + } +.dropdown-caret.right { + left: auto; + right: 10px; + } +.dropdown.dropped .dropdown-menu { + display:block; + } +.dropdown.dropped .dropdown-toggle, +.dropdown.dropped .dropdown-toggle .current-language { + color:#fff; + text-decoration:underline; + } +.dropdown.dropped .dropdown-toggle .caret { + border-top:4px solid #fff; + } + +.dropdown-menu li:not(.dropdown-caret) { + float: left; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 125px; + position: relative; + display: block; + } +.dropdown-menu li:not(.dropdown-caret) a { + clear: both; + color: #333333; + cursor: pointer; + display: block; + float: none; + font-size: 12px; + font-weight: normal; + line-height: 18px; + padding: 3px 15px 3px 22px; + position: relative; + text-shadow: none; + white-space: nowrap; + text-decoration: none; + } +.dropdown-menu li:not(.dropdown-caret) a:hover { + background-color: #2271A9; + background: -moz-linear-gradient(top, #2f7eb6 0px, #2271a9 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0px,#2f7eb6), color-stop(100%,#2271a9)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #2f7eb6 0px,#2271a9 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #2f7eb6 0px,#2271a9 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #2f7eb6 0px,#2271a9 100%); /* IE10+ */ + background: linear-gradient(to bottom, #2f7eb6 0px,#2271a9 100%); /* W3C */ + background-repeat: repeat-x; + color: #FFFFFF; + text-decoration: none; + } + +#birds-top { + display:block; + position:fixed; + z-index:1001; + margin-top:-1px; + right:-3px; + } +.topbar .global-nav { + position: relative; + width: 100%; + height: 40px; + background-color: #252525; + background-position: 0 0; + background-repeat: repeat-x; + } + +.queet-text a:hover { + text-decoration:underline; + } +.queet-text a[rel="nofollow external"] { + direction: ltr; + unicode-bidi: bidi-override; + } + +#login-content { + font-family: Arial,​sans-serif; + font-size: 14px; + color: #333333; + line-height: 18px; + background: #ffffff; /* Old browsers */ + background: -moz-linear-gradient(top, #ffffff 0px, #dddddd 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0px,#ffffff), color-stop(100%,#dddddd)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0px,#dddddd 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0px,#dddddd 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0px,#dddddd 100%); /* IE10+ */ + background: linear-gradient(to bottom, #ffffff 0px,#dddddd 100%); /* W3C */ + background-clip: border-box; + background-origin: padding-box; + background-size: auto auto; + width: 300px; + height: 108px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-right-color: #EEEEEE; + border-bottom-color: #CCCCCC; + border-left-color: #EEEEEE; + border-right-style: solid; + border-bottom-style: solid; + border-left-style: solid; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 0px 0px; + position:fixed; + opacity:0; + margin-top:1px; + z-index:2000; + } + +#login-content input#username, +#login-content input#password { + font-family: Arial,​sans-serif; + font-size: 13px; + color: #000000; + line-height: 15.4333px; + direction: ltr; + background-color: #FFFFFF; + width: 266px; + height: 20px; + top: 0px; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + padding-top: 4px; + padding-right: 4px; + padding-bottom: 4px; + padding-left: 4px; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-top-color: #CCCCCC; + border-right-color: #CCCCCC; + border-bottom-color: #CCCCCC; + border-left-color: #CCCCCC; + border-top-style: solid; + border-right-style: solid; + border-bottom-style: solid; + border-left-style: solid; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + box-shadow: 0 1px 0 #EEEEEE inset, 0 1px 0 #FFFFFF; + outline-color: #000000; + position: absolute; + display: block; + border-image-outset: 0 0 0 0; + border-image-repeat: stretch stretch; + border-image-slice: 100% 100% 100% 100%; + border-image-source: none; + border-image-width: 1 1 1 1; + outline-style: none; + outline-width: 0px; + transition-delay: 0s; + transition-duration: 0.2s; + transition-property: background; + transition-timing-function: cubic-bezier(0, 0, 1, 1); + } +#login-content input#password { + width:100%; + font-size: 13px; + color: #000000; + line-height: 16.3667px; + border-collapse: collapse; + border-spacing:0px 0px; + box-sizing:border-box; + -moz-box-sizing:border-box; + display: block; + height: 30px; + line-height: normal; + width: 100%; + position: absolute; + top: 0; + margin: 0; + outline: 0 none; + padding: 4px; + margin-left:-1px; + } + +table.password-signin { + left: 12px; + position: absolute; + top: 48px; + width: 276px; + border-collapse: collapse; + border-spacing: 0; + } +.flex-table-primary, .flex-table-secondary { + vertical-align: top; + } +.flex-table-primary { + padding-right: 5px; + width: 99%; + } +.flex-table-secondary { + max-width: 1%; + width: 1%; + vertical-align: top; + } +.placeholding-input { + width: 100%; + height: 30px; + font-size: 13px; + overflow: visible; + position: relative; + float: left; + } + +button#submit-login { + font-family: "Helvetica Neue",Arial,​sans-serif; + font-size: 13px; + font-weight: 700; + color: #FFFFFF; + line-height: 18px; + background: -moz-linear-gradient(top, #33bcef 0%, #019ad2 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#33bcef), color-stop(100%,#019ad2)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #33bcef 0%,#019ad2 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #33bcef 0%,#019ad2 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #33bcef 0%,#019ad2 100%); /* IE10+ */ + background: linear-gradient(to bottom, #33bcef 0%,#019ad2 100%); /* W3C */ + background-repeat: repeat-x; + height: 30px; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + padding-top: 5px; + padding-right: 10px; + padding-bottom: 5px; + padding-left: 10px; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-top-color: #057ED0; + border-right-color: #057ED0; + border-bottom-color: #057ED0; + border-left-color: #057ED0; + border-top-style: solid; + border-right-style: solid; + border-bottom-style: solid; + border-left-style: solid; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + box-shadow: #FFFFFF 0px 1px 0px 0px; + position: relative; + display: block; + white-space: nowrap; + border-collapse: collapse; + border-image-outset: 0 0 0 0; + border-image-repeat: stretch stretch; + border-image-slice: 100% 100% 100% 100%; + border-image-source: none; + border-image-width: 1 1 1 1; + border-spacing: 0px 0px; + cursor: pointer; + text-shadow: rgba(0, 0, 0, 0.25) 0px -1px 1px; + } +button#submit-login:hover { + background: -moz-linear-gradient(top, #2daddc 0%, #0271bf 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#2daddc), color-stop(100%,#0271bf)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #2daddc 0%,#0271bf 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #2daddc 0%,#0271bf 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #2daddc 0%,#0271bf 100%); /* IE10+ */ + background: linear-gradient(to bottom, #2daddc 0%,#0271bf 100%); /* W3C */ + background-repeat: repeat-x; + border-color:#096eb3; + color:#fff; + } + + + + + +#remember-forgot { + left: 12px; + margin: 0; + position: absolute; + top: 82px; + width: 276px; + color: #999999; + display: inline; + font-size: 11px; + line-height: 13px; + margin: 3px 0 0 0; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); + text-decoration: none; + font-family: Arial,sans-serif; + } +#remember-forgot span#rememberme_label { + cursor:pointer; + } +#remember-forgot a { + color: #999999; + display: inline; + font-size: 11px; + line-height: 13px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); + text-decoration: none; + } +#remember-forgot a:hover { + text-decoration: underline; + } +#remember-forgot input[type="checkbox"] { + height: 13px; + margin: -1px 0 0 0; + vertical-align: text-top; + } +#username-container { + color: #333333; + left: 12px; + position: absolute; + top: 12px; + width: 276px; + height: 30px; + } +#password-container { + left: 12px; + position: absolute; + top: 48px; + } + +#page-container { + width:837px; + padding:14px 14px 0 14px; + background-color:rgba(0,0,0,0.3); + margin-left:-432.5px; + opacity:0; + } + +#footer { + width:100%; + clear:both; + height:100px; + } + +#user-container { + width:300px; + font-size: 12px; + text-shadow: 0 1px 0 #FFFFFF; + line-height: 16px; + position: relative; + float:left; + } +#user-header, +#user-body, +#user-footer { + border: 1px solid rgba(0, 0, 0, 0.1); + opacity:0; + } +#user-header { + border-bottom:0 none; + border-radius: 6px 6px 0 0; + } +#user-body { + border-top:0 none; + border-bottom:0 none; + } +#user-footer { + border-top:0 none; + border-radius: 0 0 6px 6px; + margin-bottom:10px; + } +#user-header:hover #user-name { + color: #0084B4; + text-decoration:underline; + } + +.menu-container { + border: 1px solid rgba(0, 0, 0, 0.1); + text-shadow: 0 1px 0 #FFFFFF; + background-clip: padding-box; + border-radius: 6px 6px 6px 6px; + line-height: 16px; + margin-bottom: 10px; + overflow:hidden; + opacity:0; + } +.menu-container div:first-child { + border-top-left-radius: 6px; + border-top-right-radius: 6px; + } +.menu-container div:last-child { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + } +.menu-container div { + cursor:pointer; + background-color: #FFFFFF; + color: #0084B4; + text-decoration: none; + padding: 8px 12px; + position: relative; + background-color: #F9F9F9; + border-top: 1px solid #E8E8E8; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.25) inset; + font-size: 14px; + } +.menu-container div .chev-right { + background-repeat: no-repeat; + display: block; + height: 13px; + width: 9px; + background-position: 0 -140px; + position: absolute; + right: 12px; + top: 9px; + } +.menu-container div .close-right { + background-repeat: no-repeat; + display: none; + height: 9px; + width: 13px; + background-position: -20px -510px; + margin-left:5px; + margin-top:4px; + } +.menu-container div:hover { + background-color:#fff; + } +.menu-container div:hover .chev-right { + background-position: 0 -160px; + } +.menu-container div:hover .close-right { + display:inline-block; + } +.menu-container div .close-right:hover { + background-position: -40px -510px; + } +.menu-container div.current { + background-color:#fff; + font-weight:bold; + color:#333; + } +.menu-container div.current .chev-right { + background-position: 0 -160px; + } +#history-container { + display:none; + } + +#feed { + display:none; + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 14px; + color: #333333; + line-height: 18px; + width: 522px; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + float: right; + } + +#feed-header { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 14px; + color: #333333; + line-height: 18px; + background-clip: padding-box; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 0px; + border-left-width: 1px; + border-top-color: rgba(0, 0, 0,0.1); + border-right-color: rgba(0,0,0,0.1); + border-bottom-color: rgba(0, 0, 0, 0.1); + border-left-color: rgba(0, 0, 0, 0.1); + border-top-style: solid; + border-right-style: solid; + border-bottom-style: solid; + border-left-style: solid; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + border-image-outset: 0 0 0 0; + border-image-repeat: stretch stretch; + border-image-slice: 100% 100% 100% 100%; + border-image-source: none; + border-image-width: 1 1 1 1: + } +#feed-header-inner { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 14px; + color: #333333; + line-height: 18px; + background-color: #FFFFFF; + padding-top: 12px; + padding-right: 12px; + padding-bottom: 12px; + padding-left: 12px; + border-bottom-width: 1px; + border-bottom-color: #E8E8E8; + border-bottom-style: solid; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + min-height: 20px; + } + +#feed-header-inner h2 { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 18px; + font-weight: 700; + color: #333333; + line-height: 20px; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + text-rendering: optimizelegibility; + } + +.queet-streams { + bottom: 0; + color: #999999; + display:inline-block; + font-size: 12px; + font-weight: normal; + margin: 3px 6px; + } +.queet-streams a { + text-decoration: none; + cursor:pointer; + } +.queet-streams a:hover { + text-decoration: underline; + } + +#new-queets-bar { + background-color: #F5F5F5; + border-top: 1px solid #DDDDDD; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.05) inset; + cursor: pointer; + display: block; + font-size: 13px; + font-weight: normal; + padding: 10px 1px; + position: relative; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.6); + top: -1px; + z-index: 2; + color: #0084B4; + } +#new-queets-bar:hover { + background-color: #eee; + } + +.stream-item { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 14px; + color: #333333; + line-height: 18px; + background-color: #FFFFFF; + background-image: none; + background-repeat: repeat; + background-position: 0% 0%; + background-attachment: scroll; + background-clip: padding-box; + background-origin: padding-box; + background-size: auto auto; + border-right-width: 1px; + border-left-width: 1px; + border-right-color: rgba(0, 0, 0, 0.1); + border-left-color: rgba(0, 0, 0, 0.1); + border-right-style: solid; + border-left-style: solid; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + -webkit-transition: all 0.1s ease; + -moz-transition: all 0.1s ease; + -o-transition: all 0.1s ease; + transition: all 0.1s ease; + } +.stream-item.hidden { + display:none; + } +.stream-item.conversation { + background-color:#F6F6F6; + border:0 none; + opacity:0.5; + } +.stream-item.conversation.visible { + opacity:1; + } +.stream-item.conversation.hidden-conversation { + display:none; + overflow:hidden; + } +.stream-item.conversation .queet:hover { + background-color:#F6F6F6; + } +.show-full-conversation { + float:right; + font-style:italic; + } +.show-full-conversation:hover { + color:#0084B4; + text-decoration:underline; + } +.queet { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 14px; + color: #333333; + line-height: 18px; + border-bottom-width: 1px; + border-bottom-color: #E8E8E8; + border-bottom-style: solid; + position: relative; + cursor: pointer; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + min-height: 73px; + } +.stream-item.user .queet { + cursor: auto; + } +.queet:hover { + background-color:#f5f5f5; + } +.stream-item.expanded .queet { + border-bottom:1px solid #ddd; + } +.stream-item.expanded .queet:hover { + background-color:#fff; + } +.stream-item.expanded .stream-item:last-child .queet { + border-bottom:0 none; + } +.stream-item.expanded div:last-child { + border-radius:0 0 6px 6px; + } +body.rtl .queet.rtl .expanded-content { + direction:rtl; + } +body.rtl .queet:not(.rtl) .client-and-actions { + direction:rtl; + text-align:left; + } +body.rtl .queet:not(.rtl) .stream-item-header { + direction: rtl; + text-align: left; + } +.stream-item.expanded div:first-child { + border-radius:6px 6px 0 0; + } +.stream-item.expanded .stream-item.conversation .queet:hover { + background-color:#F6F6F6; + } +.queet:hover .stream-item-expand { + color:#0084B4; + } +.queet:hover .stream-item-expand:hover { + text-decoration:underline; + } + +.stream-item.activity .queet { + min-height:21px; + } +.stream-item.activity .queet-content { + margin-left:0; + } +.stream-item.activity .queet-text { + font-size: 0.85em; + font-style: italic; + margin:0; + text-align:center; + width: 470px; + } + +.stream-item.activity .created-at a { + display:none; + } +.stream-item.activity.expanded .created-at a { + font-size: 0.9em; + display:inline; + } +.view-more-container-top { + border-top: 0 none; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + background: none repeat scroll 0 0 #F6F6F6; + cursor:pointer; + border-bottom: 1px solid #DDDDDD; + } +.view-more-container-bottom { + border-bottom: 0 none; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + background: none repeat scroll 0 0 #F6F6F6; + cursor:pointer; + } +body.rtl .view-more-container-top { direction:rtl; } +body.rtl .view-more-container-bottom { direction:rtl; } +.view-more-container-top a, +.view-more-container-bottom a { + display:block; + font-size: 13px; + padding: 14px 12px 14px 72px; + } +.view-more-container-top:hover a, +.view-more-container-bottom:hover a { + text-decoration:underline; + } + +.queet-content { + margin-left: 58px; + cursor:pointer; + padding-top: 9px; + padding-right: 12px; + padding-bottom: 9px; + padding-left: 12px; + + } +.stream-item.user .queet-content { + cursor: auto; + } + +.stream-item-header { + color:#999; + } + +.stream-item-header a.account-group:hover .name { + text-decoration:underline; + color:#0084B4; + } + +.stream-item-header .avatar { + line-height: 18px; + width: 48px; + height: 48px; + margin-top: 3px; + margin-left: -58px; + border-top-width: 0px; + border-right-width: 0px; + border-bottom-width: 0px; + border-left-width: 0px; + border-top-color: #999999; + border-right-color: #999999; + border-bottom-color: #999999; + border-left-color: #999999; + border-top-style: none; + border-right-style: none; + border-bottom-style: none; + border-left-style: none; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + float: left; + border-image-outset: 0 0 0 0; + border-image-repeat: stretch stretch; + border-image-slice: 100% 100% 100% 100%; + border-image-source: none; + border-image-width: 1 1 1 1; + cursor: pointer; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + } +.stream-item-header .name { + font-family: "Helvetica Neue",Arial,sans-serif; + font-size: 14px; + font-weight: 700; + color: #333333; + line-height: 18px; + } +.stream-item-header .screen-name { + font-family: "Helvetica Neue",Arial,sans-serif; + font-size: 12px; + color: #999999; + line-height: 18px; + direction: ltr; + } +.stream-item-header .created-at { + font-family: "Helvetica Neue",Arial,sans-serif; + font-size: 12px; + color: #BBBBBB; + line-height: 18px; + margin-top: 1px; + position: relative; + float: right; + } +.stream-item-header .created-at a { + font-family:"Helvetica Neue",Arial,sans-serif; + font-size: 12px; + color: #999999; + text-decoration: none; + line-height: 18px; + } +.stream-item-header .created-at a:hover { + text-decoration: underline; + color:#0084B4; + } + +.dogear { + font-family: "Helvetica Neue",Arial,sans-serif; + font-size: 14px; + color: #333333; + line-height: 18px; + vertical-align: text-top; + background-repeat: no-repeat; + width: 24px; + height: 24px; + top: 0px; + right: 0px; + position: absolute; + display: none; + cursor: pointer; + list-style-image: none; + list-style-position: outside; + list-style-type: none + } +.stream-item.requeeted > .queet .dogear { + display: block; + background-position: 0 -450px; + } +.stream-item.favorited > .queet .dogear { + display: block; + background-position: -30px -450px; + } +.stream-item.requeeted.favorited > .queet .dogear { + display: block; + background-position: -60px -450px; + } + +.queet-text { + font-family:"Helvetica Neue",Arial,sans-serif; + font-size: 14px; + color: #333333; + line-height: 18px; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + white-space: pre-wrap; + cursor: pointer; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + word-wrap: break-word; + } +.stream-item.user .queet-text { + cursor: auto; + } + +.queet-text img { + width:100%; + } + +.stream-item .queet-text p { + margin:0;padding:0; + } + +.queet-text span.attachment.more { + color:#0084B4; + } +.queet-text span.attachment.more:hover { + text-decoration:underline; + } + +.stream-item.user .queet-content { + margin-right:150px; + } +.stream-item.user .user-actions { + float:right; + padding:9px 12px; + } +body.rtl .stream-item.user .user-actions { + float:left; + } +body.rtl .stream-item.user .queet-content { + margin-left:150px; + margin-right:58px; + } +body.rtl .stream-item.user .queet-text, +body.rtl .stream-item.user .queet-content .stream-item-header { + text-align:right; + } +body.rtl .stream-item.user .queet-content .stream-item-header .avatar { + float:right; + margin-left:auto; + margin-right:-58px; + } + + +ul.queet-actions { + bottom: 0; + font-family: "Helvetica Neue",Arial,sans-serif; + font-size: 12px; + color: #333333; + line-height: 18px; + background-color: #FFFFFF; + right: 1px; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + padding-top: 0px; + padding-right: 0px; + padding-bottom: 0px; + padding-left: 5px; + position: absolute; + display: none; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + } +.queet.rtl ul.queet-actions { + right:auto; + left:1px; + padding: 0 5px 0 0; + direction: rtl; + } + +.stream-item:hover > .queet ul.queet-actions { + display: block; + } +.stream-item.expanded > .queet ul.queet-actions { + display: block; + } +.stream-item.expanded > .stream-item.expanded .queet ul.queet-actions { + display: block; + background-color: #f6f6f6; + } +.stream-item:hover:not(.expanded) ul.queet-actions { + background-color: #f6f6f6; + } +ul.queet-actions li { + display:inline; + } +ul.queet-actions li .with-icn { + color: #0084B4; + margin-left: 8px; + } +ul.queet-actions li .with-icn b { + display: inline; + color: inherit; + font-weight: normal; + } +ul.queet-actions li .with-icn b:hover { + text-decoration:underline; + } +ul.queet-actions li .icon { + background-color: #0084B4; + background-repeat: no-repeat; + display: inline-block; + vertical-align: text-top; + height: 13px; + } +ul.queet-actions li.action-rt-container .with-icn.done .icon { + background-color: #609928; + } +ul.queet-actions li.action-rt-container .with-icn.done { + color: #609928; + } +ul.queet-actions li.action-fav-container .with-icn.done .icon { + background-color: #FF9B00; + } +ul.queet-actions li.action-fav-container .with-icn.done { + color: #FF9B00; + } + +ul.queet-actions li .icon.sm-reply { + background-position: 0 -190px; + width: 12px; + } +ul.queet-actions li .icon.sm-rt { + background-position: -20px -190px; + width: 12px; + } +ul.queet-actions li .icon.sm-trash { + background-position: -160px -190px; + width: 12px; + } +ul.queet-actions li .icon.sm-fav { + background-position: -40px -190px; + width: 12px; + } +.stream-item:not(.expanded):hover ul.queet-actions li .icon.sm-reply, +.stream-item.expanded > .stream-item.expanded .queet ul.queet-actions li .icon.sm-reply { + background-position: 0 -220px; + } +.stream-item:not(.expanded):hover ul.queet-actions li .icon.sm-rt, +.stream-item.expanded > .stream-item.expanded .queet ul.queet-actions li .icon.sm-rt { + background-position: -20px -220px; + } +.stream-item:not(.expanded):hover ul.queet-actions li .icon.sm-trash, +.stream-item.expanded > .stream-item.expanded .queet ul.queet-actions li .icon.sm-trash { + background-position: -160px -220px; + } +.stream-item:not(.expanded):hover ul.queet-actions li .icon.sm-fav, +.stream-item.expanded > .stream-item.expanded .queet ul.queet-actions li .icon.sm-fav { + background-position: -40px -220px; + } + + + +.queet.rtl .queet-content { + margin-left:0; + margin-right:58px; + } +.queet.rtl .account-group > .avatar { + float:right; + margin-left:0; + margin-right:-58px; + } +.queet.rtl .stream-item-header { + direction: rtl; + text-align:right; + } +.queet.rtl .account-group > .name { + direction: rtl; + text-align:right; + } +.queet.rtl .account-group > .screen-name { + direction: rtl; + text-align:right; + } +.queet.rtl .created-at { + float:left; + } +.queet.rtl .queet-text { + text-align:right; + direction: rtl; + } +.queet.rtl .stream-item-footer { + text-align: right; + } +.queet.rtl .client-and-actions { + text-align: right; + } +.queet.rtl .show-full-conversation { + float:left; + } +.queet.rtl .inline-reply-queetbox { + padding:15px 70px 15px 12px; + } +.queet-box-template a{ + unicode-bidi:bidi-override; + direction:ltr; + } + +.stream-item-footer { + padding-top:1px; + position:relative; + } + +.stream-item-footer .context { + font-size:14px; + } +.stream-item-footer .with-icn { + font-size:12px; + color:#999; + } +.stream-item-footer .with-icn .badge-requeeted { + font-family: "Helvetica Neue",Arial,sans-serif; + font-size: 12px; + color: #999999; + line-height: 18px; + vertical-align: -1px; + background-color: transparent; + background-repeat: no-repeat; + background-position: -60px -350px; + width: 16px; + height: 12px; + margin-right: 1px; + display: inline-block; + cursor: pointer; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + } +.stream-item-footer .with-icn .requeet-text { + font-size:12px; + color:#999; + } +.stream-item-footer .with-icn .requeet-text a b { + color:#999; + font-weight:400; + } +.stream-item-footer .with-icn .requeet-text a b:hover { + text-decoration:underline; + color:#0084B4; + } + +.stream-item-expand { + font-size: 12px; + font-weight: 400; + color: #999999; + line-height: 18px; + } + +.stream-item.expanded { + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 6px 6px 6px 6px; + margin:8px 0; + overflow:hidden; + -webkit-transition: all 0.1s ease; + -moz-transition: all 0.1s ease; + -o-transition: all 0.1s ease; + transition: all 0.1s ease; + } +.stream-item.expanded > .queet .stream-item-expand { + color: #0084B4; + } +.stream-item.expanded .stream-item-expand:hover { + text-decoration:underline; + } +.stream-item.expanded .stream-item.expanded { + margin:0 0; + border:0 none; + border-radius: 0 0 0 0; + } + +.stream-item .media { + margin-top:10px; + } +.stream-item .media img { + max-width: 435px; + } + +ul.stats { + font-family: "Helvetica Neue",Arial,sans-serif; + font-size: 14px; + color: #333333; + line-height: 18px; + margin-top: 10px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + padding-top: 0px; + padding-right: 0px; + padding-bottom: 0px; + padding-left: 0px; + border-top: 1px solid #E8E8E8; + border-bottom: 1px solid #E8E8E8; + overflow-x: hidden; + overflow-y: hidden; + cursor: pointer; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + overflow: hidden; + } +.queet.rtl ul.stats { + text-align:right; + } +ul.stats li { + display:inline; + } +.queet.rtl ul.stats li { + float:right; + } +ul.stats a { + width: auto; + border-left: 1px solid #E8E8E8; + color: #999999; + float: left; + font-size: 10px; + line-height: 16px; + padding: 7px 12px; + text-transform: uppercase; + } +.queet.rtl ul.stats a{ + border-right: 1px solid #E8E8E8; + border-left: 0 none; + } +ul.stats li:first-child a { + padding-left: 0; + } +.queet.rtl ul.stats li:first-child a { + padding-right: 0; + padding-left: 12px; + } +ul.stats li:first-child a { + border-left: 0 none; + } +.queet.rtl ul.stats li:first-child a { + border-right: 0 none; + } + + +ul.stats a strong { + color: #333333; + display: block; + font-size: 14px; + } + +ul.stats .avatar-row { + overflow: hidden; + } +ul.stats .avatar-row a { + padding: 11px 0; + border-left: medium none; + } +ul.stats .avatar-row a:first-child { + border-left: 1px solid #E8E8E8; + padding-left: 12px; + } +.queet.rtl ul.stats .avatar-row a { + border-right: 0 none; + padding-right: 0; + border-left: 0 none; + padding-left: 0; + } +.queet.rtl ul.stats .avatar-row a:last-child { + border-right: 1px solid #E8E8E8; + padding-right: 12px; + border-left: 0 none; + padding-left: 0; + } +ul.stats .avatar-row .avatar { + float: left; + margin-right: 5px; + } +.queet.rtl ul.stats .avatar-row .avatar { + float: left; + margin-left: 5px; + margin-right: 0; + } +.avatar.size24 { + border-radius: 3px 3px 3px 3px; + height: 24px; + width: 24px; + } +.avatar.size30 { + border-radius: 3px 3px 3px 3px; + height: 30px; + width: 30px; + margin:0 1px; + } +ul.stats .avatar-row .avatar { + -moz-force-broken-image-icon: 1; + } + +.client-and-actions { + clear: both; + margin-top: 10px; + overflow: hidden; + } + +.client-and-actions .metadata { + line-height: 24px; + overflow: hidden; + color: #999999; + font-size: 12px; + line-height: 24px; + } + +.permalink-link { + color: #999999; + } +.permalink-link:hover { + text-decoration:underline; + color:#0084B4; + } + +.inline-reply-queetbox { + padding: 15px 12px 15px 70px; + position: relative; + background: none repeat scroll 0 0 #F6F6F6; + border-top:1px solid #DDDDDD; + } +.queet-box-template { + font-family: "Helvetica Neue",Arial,​sans-serif; + font-size: 13px; + color: #AAAAAA; + line-height: 20px; + vertical-align: top; + text-overflow: ellipsis; + background-color: #FFFFFF; + width: 420px; + height: 19px; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + padding-top: 8px; + padding-right: 8px; + padding-bottom: 8px; + padding-left: 8px; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-top-color: #CCCCCC; + border-right-color: #CCCCCC; + border-bottom-color: #CCCCCC; + border-left-color: #CCCCCC; + border-top-style: solid; + border-right-style: solid; + border-bottom-style: solid; + border-left-style: solid; + border-top-left-radius: 3px !important; + border-top-right-radius: 3px !important; + border-bottom-left-radius: 3px !important; + border-bottom-right-radius: 3px !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) inset, 0 1px 0 rgba(255, 255, 255, 0.075); + outline-color: #AAAAAA; + display: block; + overflow-x: hidden; + overflow-y: hidden; + border-image-outset: 0 0 0 0; + border-image-repeat: stretch stretch; + border-image-slice: 100% 100% 100% 100%; + border-image-source: none; + border-image-width: 1 1 1 1; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + outline-style: none; + outline-width: 0px; + overflow: hidden; + text-shadow: none; + word-wrap: normal; + cursor:text; + white-space:nowrap; + } +.queet-box-template[contenteditable="false"] { + opacity:0.5; + } + +.queet-box-template:focus { border-color: #56B4EF; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) inset, 0 0 8px rgba(82, 168, 236, 0.6); color: #333333; outline: 0 none; + } +.queet-box-template.active { + height:80px; + color:#333; + margin-bottom:8px; + white-space:normal; + -webkit-transition: all 0.3s ease; + -moz-transition: all 0.3s ease; + -o-transition: all 0.3s ease; + transition: all 0.3s ease; + } + +#user-header { + border-top-left-radius: 5px; + border-top-right-radius: 5px; + background-color: #F9F9F9; + line-height: 16px; + padding: 12px; + cursor: pointer; + font-size: 12px; + text-shadow: 0 1px 0 #FFFFFF; + line-height: 16px; + font-family: "Helvetica Neue",Arial,sans-serif; + color: #333333; + font-size: 14px; + line-height: 18px; + } +#user-name { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 14px; + font-weight: 700; + color: #333333; + line-height: 16px; + display: block; + cursor: pointer; + text-shadow: #FFFFFF 0px 1px 0px; + margin-left:42px; + margin-right:20px; + } +#user-screen-name { + display:none; + } + +#user-profile-link a { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 11px; + color: #999999; + line-height: 16px; + text-overflow: ellipsis; + overflow-x: hidden; + overflow-y: hidden; + white-space: nowrap; + cursor: pointer; + overflow: hidden; + text-shadow: #FFFFFF 0px 1px 0px; + margin-left:42px; + margin-right:20px; + } + +#user-avatar { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 12px; + color: #FF00AE; + line-height: 16px; + width: 32px; + height: 32px; + top: 12px; + left: 12px; + border-top-width: 0px; + border-right-width: 0px; + border-bottom-width: 0px; + border-left-width: 0px; + border-top-color: #FF00AE; + border-right-color: #FF00AE; + border-bottom-color: #FF00AE; + border-left-color: #FF00AE; + border-top-style: none; + border-right-style: none; + border-bottom-style: none; + border-left-style: none; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + position: absolute; + text-shadow: #FFFFFF 0px 1px 0px: + } + +#user-body { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 12px; + color: #333333; + line-height: 16px; + background-color: #F9F9F9; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + padding-top: 0px; + padding-right: 0px; + padding-bottom: 0px; + padding-left: 0px; + border-top-width: 1px; + border-bottom-width: 1px; + border-top-color: #E8E8E8; + border-bottom-color: #E8E8E8; + border-top-style: solid; + border-bottom-style: solid; + overflow-x: hidden; + overflow-y: hidden; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + overflow: hidden; + text-shadow: #FFFFFF 0px 1px 0px; + } + +#user-body #user-queets, +#user-body #user-groups, +#user-body #user-following, +#user-body #user-followers { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 9.5px; + color: #999999; + text-transform: uppercase; + text-decoration: none; + line-height: 16px; + padding-top: 7px; + padding-right: 10px; + padding-bottom: 7px; + padding-left: 12px; + border-left-width: 0px; + border-left-color: #999999; + border-left-style: none; + float: left; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + text-shadow: #FFFFFF 0px 1px 0px; + } +#user-body #user-following, +#user-body #user-groups, +#user-body #user-followers { + border-left-width: 1px; + border-left-color: #E8E8E8; + border-left-style: solid; + } + +#user-body strong { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 14px; + color: #333333; + text-transform: uppercase; + line-height: 16px; + display: block; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + text-shadow: #FFFFFF 0px 1px 0px; + } +#user-body a { + cursor:pointer; + } +ul.stats li:hover a, +ul.stats li:hover a strong, +#user-body a:hover div strong, +#user-body a:hover div div { + color:#0084B4; + } + +#user-footer { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 12px; + color: #333333; + line-height: 12px; + background-color: #F5F5F5; + padding-top: 10px; + padding-right: 12px; + padding-bottom: 10px; + padding-left: 12px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + text-shadow: #FFFFFF 0px 1px 0px; + } + +.queet-box { + font-family: "Helvetica Neue",​Arial,​sans-serif; + font-size: 13px; + color: #AAAAAA; + line-height: 18px; + vertical-align: top; + background-color: #FFFFFF; + width: 258px; + height: 19px; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + padding-top: 6px; + padding-right: 8px; + padding-bottom: 5px; + padding-left: 8px; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-top-color: #CCCCCC; + border-right-color: #CCCCCC; + border-bottom-color: #CCCCCC; + border-left-color: #CCCCCC; + border-top-style: solid; + border-right-style: solid; + border-bottom-style: solid; + border-left-style: solid; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) inset, 0 1px 0 rgba(255, 255, 255, 0.075); + outline-color: #AAAAAA; + display: block; + overflow-x: hidden; + overflow-y: hidden; + border-image-outset: 0 0 0 0; + border-image-repeat: stretch stretch; + border-image-slice: 100% 100% 100% 100%; + border-image-source: none; + border-image-width: 1 1 1 1; + outline-style: none; + outline-width: 0px; + overflow: hidden; + text-shadow: none; + word-wrap: break-word; + } +#queet-box[contenteditable="true"] { + font-family: "Helvetica Neue",Arial,​sans-serif; + font-size: 13px; + color: #333333; + line-height: 18px; + vertical-align: top; + background-color: #FFFFFF; + width: 258px; + height: 80px; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 8px; + margin-left: 0px; + padding-top: 6px; + padding-right: 8px; + padding-bottom: 5px; + padding-left: 8px; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-top-color: #CCCCCC; + border-right-color: #CCCCCC; + border-bottom-color: #CCCCCC; + border-left-color: #CCCCCC; + border-top-style: solid; + border-right-style: solid; + border-bottom-style: solid; + border-left-style: solid; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) inset, 0 1px 0 rgba(255, 255, 255, 0.075); + outline-color: #333333; + display: block; + overflow-x: hidden; + overflow-y: auto; + border-image-outset: 0 0 0 0; + border-image-repeat: stretch stretch; + border-image-slice: 100% 100% 100% 100%; + border-image-source: none; + border-image-width: 1 1 1 1; + outline-style: none; + outline-width: 0px; + text-shadow: none; + word-wrap: break-word; + } +#queet-box[contenteditable="true"]:focus { + box-shadow:0 1px 3px rgba(0, 0, 0, 0.05) inset, 0 0 8px rgba(82, 168, 236, 0.6); + color:##333333; + outline:0 none; + border-color:#56B4EF; + } + +#queet-toolbar, +#queet-toolbar { + position:relative; + display:none; + height:32px; + } + +#queet-box-extras, +.#queet-box-extras { + float: left; + } + +#queet-button, +.queet-button { + float: right; + } +.queet.rtl .queet-button { + float: left; + } +.queet.rtl .queet-button .queet-counter { + float: right; + text-align:left; + margin-left:5px; + margin-right:0; + } + +#queet-counter, +.queet-counter { + background-color: transparent; + border: 0 none; + color: #999999; + display: inline-block; + font-size: 14px; + padding: 0 3px; + position: relative; + text-align: right; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + top: 7px; + vertical-align: top; + width: 35px; + margin-right: 5px; + } + +#queet-toolbar button, +.queet-toolbar button { + font-family: "Helvetica Neue",Arial,sans-serif; + font-size: 13px; + font-weight: 700; + color: #FFFFFF; + line-height: 16px; + background: -moz-linear-gradient(top, #33bcef 0%, #019ad2 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#33bcef), color-stop(100%,#019ad2)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #33bcef 0%,#019ad2 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #33bcef 0%,#019ad2 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #33bcef 0%,#019ad2 100%); /* IE10+ */ + background: linear-gradient(to bottom, #33bcef 0%,#019ad2 100%); /* W3C */ + background-repeat: repeat-x; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + padding-top: 5px; + padding-right: 10px; + padding-bottom: 5px; + padding-left: 10px; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-top-color: #057ED0; + border-right-color: #057ED0; + border-bottom-color: #057ED0; + border-left-color: #057ED0; + border-top-style: solid; + border-right-style: solid; + border-bottom-style: solid; + border-left-style: solid; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 0px 0px inset; + position: relative; + display: inline-block; + border-image-outset: 0 0 0 0; + border-image-repeat: stretch stretch; + border-image-slice: 100% 100% 100% 100%; + border-image-source: none; + border-image-width: 1 1 1 1; + cursor: pointer; + text-shadow: rgba(0, 0, 0, 0.25) 0px -1px 1px; + } +#queet-toolbar button.disabled, +.queet-toolbar button.disabled { + background-color: #DDDDDD; + background-image: none; + border-color: #CCCCCC; + color: #777777; + cursor: default; + opacity: 0.65; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + } +#queet-toolbar button.enabled:hover, +.queet-toolbar button.enabled:hover { + background: -moz-linear-gradient(top, #2daddc 0%, #0271bf 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#2daddc), color-stop(100%,#0271bf)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #2daddc 0%,#0271bf 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #2daddc 0%,#0271bf 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #2daddc 0%,#0271bf 100%); /* IE10+ */ + background: linear-gradient(to bottom, #2daddc 0%,#0271bf 100%); /* W3C */ + background-repeat: repeat-x; + border-color:#096eb3; + color:#fff; + } + +#logo { + width:89px; + height:34px; + display:block; + position:fixed; + z-index:1001; + margin-top:3px; + left:50%; + margin-left:-420px; + background-position:0 0; + cursor:pointer; + } +#logolink:hover #logo { + background-position: 0 34px; + } +#logolink:hover i.nav-session { + background-position: -160px -80px; + } +#logolink:hover .caret { + border-top:4px solid #fff; + } + +#logolink .dropdown-toggle { + left: 50%; + margin-left: -340px; + position: fixed; + z-index: 1001; + } +#logolink .caret { + display:block; + position:absolute; + margin-left:25px; + top:13px; + } +.nav-session { + line-height: 1; + text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.75); + background-repeat: no-repeat; + vertical-align: text-top; + background-position: -160px -50px; + height: 21px; + width: 20px; + margin: -4px 6px 0 0; + cursor: pointer; + display: block; + } + + +.profile-card { + background-clip: padding-box; + border-radius: 6px 6px 6px 6px; + line-height: 16px; + margin-bottom: 10px; + border: 1px solid rgba(0, 0, 0, 0.1); + text-shadow: 0 1px 0 #FFFFFF; + float: right; + position: relative; + width: 520px; + } +.modal-body .profile-card { + margin-right:-1px; + margin-top:-5px; + margin-bottom:0; + } +.profile-header-inner { + text-shadow: 0 1px 0 #FFFFFF; + line-height: 16px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + background-color: #444444; + background-repeat: no-repeat; + box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.1) inset; + height: 260px; + overflow: hidden; + padding: 0; + text-align: center; + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; + } +.profile-header-inner .profile-header-inner-overlay { + background: -moz-linear-gradient(top, rgba(0,0,0,0) 0%, rgba(0,0,0,0.65) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(0,0,0,0)), color-stop(100%,rgba(0,0,0,0.65))); + background: -webkit-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(0,0,0,0.65) 100%); + background: -o-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(0,0,0,0.65) 100%); + background: -ms-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(0,0,0,0.65) 100%); + background: linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(0,0,0,0.65) 100%); + height: 200px; + position: absolute; + top: 60px; + width: 520px; + } +.profile-header-inner .profile-picture { + outline: 0 none; + background-color: #FFFFFF; + border: 4px solid #FFFFFF; + border-radius: 5px 5px 5px 5px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.25); + display: block; + height: 73px; + margin: 20px auto 6px; + position: relative; + width: 73px; + z-index: 5; + } +.profile-header-inner .profile-picture img { + height: 73px; + width: 73px; + border-radius: 3px 3px 3px 3px; + float: none; + } +.profile-header-inner .profile-card-inner { + text-align: center; + line-height: 16px; + float: none; + margin: auto; + position: relative; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); + width: 85%; + } +.profile-header-inner .profile-card-inner h1.fullname { + text-rendering: optimizelegibility; + font-weight: bold; + line-height: 1; + margin-bottom: 2px; + color: #FFFFFF; + font-size: 24px; + white-space: nowrap; + margin-top:0; + } +.profile-header-inner .profile-card-inner h1.fullname span { + font-size: 24px; + white-space: nowrap; + line-height: 1; + } +.profile-header-inner .profile-card-inner h2.username { + margin-top:0; + color: #FFFFFF; + font-size: 18px; + font-weight: normal; + line-height: 24px; + margin-bottom: 1px; + text-rendering: optimizelegibility; + } +.profile-header-inner .profile-card-inner h2.username a { + color:#fff; + } +.profile-header-inner .profile-card-inner .bio-container { + line-height: 18px; + margin-bottom: 4px; + font-size: 14px; + } +.profile-header-inner .profile-card-inner .bio-container p { + color: #FFFFFF; + margin:0; + } +.profile-header-inner .profile-card-inner .location-and-url { + font-size: 14px; + color: #FFFFFF; + line-height: 18px; + margin:0; + } +.profile-header-inner .profile-card-inner .location-and-url span.divider { + padding: 0 2px; + } +.profile-header-inner .profile-card-inner .location-and-url span a { + color:#fff; + } +.profile-header-inner .profile-card-inner .location-and-url span a:hover { + text-decoration:underline; + } +.profile-banner-footer { + padding: 0; + background-color: #FFFFFF; + line-height: 16px; + text-shadow: 0 1px 0 #FFFFFF; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } +.profile-banner-footer ul.stats { + border-bottom: 0 none; + border-top: 0 none; + margin: 0; + float:left; + } +.profile-banner-footer ul.stats li a { + padding: 10px 15px 8px 12px; + } +.profile-banner-footer ul.stats li:first-child a { + padding-left: 12px; + } +.profile-banner-footer ul.stats li a strong { + } + +.profile-card .user-actions { + position:relative; + float: right; + margin: 10px; + } +div.clearfix { + clear:both; + height:0; + } + +.member-button, +.follow-button { + font-family: "Helvetica Neue",Arial,sans-serif; + margin: 0; + position: relative; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); + border: 1px solid #CCCCCC; + border-radius: 4px 4px 4px 4px; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + color: #333333; + cursor: pointer; + display: inline-block; + font-size: 13px; + font-weight: bold; + line-height: 18px; + background-color: #DDDDDD; + background: -moz-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(221,221,221,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,1)), color-stop(100%,rgba(221,221,221,1))); + background: -webkit-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(221,221,221,1) 100%); + background: -o-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(221,221,221,1) 100%); + background: -ms-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(221,221,221,1) 100%); + background: linear-gradient(to bottom, rgba(255,255,255,1) 0%,rgba(221,221,221,1) 100%); + background-repeat: repeat-x; + padding: 0; + } +.member-button.disabled, +.follow-button.disabled { + color:#ccc; + } +.member-button.disabled i, +.follow-button.disabled i { + opacity:0.2; + } +.member-button:not(.disabled):not(.member):hover, +.follow-button:not(.disabled):not(.following):hover { + background-color: #D8D8D8; + background: -moz-linear-gradient(top, rgba(248,248,248,1) 0%, rgba(216,216,216,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(248,248,248,1)), color-stop(100%,rgba(216,216,216,1))); + background: -webkit-linear-gradient(top, rgba(248,248,248,1) 0%,rgba(216,216,216,1) 100%); + background: -o-linear-gradient(top, rgba(248,248,248,1) 0%,rgba(216,216,216,1) 100%); + background: -ms-linear-gradient(top, rgba(248,248,248,1) 0%,rgba(216,216,216,1) 100%); + background: linear-gradient(to bottom, rgba(248,248,248,1) 0%,rgba(216,216,216,1) 100%); + border-color: #BBBBBB; + text-decoration: none; + } +.member-button:not(.disabled):not(.member):active, +.follow-button:not(.disabled):not(.following):active { + background: none; + background-color: #D8D8D8; + border-color: #BBBBBB; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) inset, 0 1px 0 rgba(255, 255, 255, 0.5); + } +.member-button .button-text, +.follow-button .button-text { + display:none; + font-family: "Helvetica Neue",Arial,sans-serif; + min-width: 70px; + padding: 5px 7px; + text-align: center; + } +.member-button .join-text, +.follow-button .follow-text { + padding: 5px 20px 5px 60px; + text-align:right; + min-width: 0; + } +.member-button .join-text i, +.follow-button .follow-text i { + background-image: url("../img/button_birds.png"); + display: block; + height: 28px; + margin-left: -50px; + margin-top: -5px; + position: absolute; + width: 40px; + } +.member-button.member, +.follow-button.following { + background-color: #019AD2; + background: -moz-linear-gradient(top, rgba(51,188,239,1) 0%, rgba(1,154,210,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(51,188,239,1)), color-stop(100%,rgba(1,154,210,1))); + background: -webkit-linear-gradient(top, rgba(51,188,239,1) 0%,rgba(1,154,210,1) 100%); + background: -o-linear-gradient(top, rgba(51,188,239,1) 0%,rgba(1,154,210,1) 100%); + background: -ms-linear-gradient(top, rgba(51,188,239,1) 0%,rgba(1,154,210,1) 100%); + background: linear-gradient(to bottom, rgba(51,188,239,1) 0%,rgba(1,154,210,1) 100%); + background-repeat: repeat-x; + border-color: #057ED0; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1) inset; + color: #FFFFFF; + text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25); + } +.member-button.member:hover, +.follow-button.following:hover { + background-color: #c43c35; + background: -moz-linear-gradient(top, rgba(238,95,91,1) 0%, rgba(196,60,53,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(238,95,91,1)), color-stop(100%,rgba(196,60,53,1))); + background: -webkit-linear-gradient(top, rgba(238,95,91,1) 0%,rgba(196,60,53,1) 100%); + background: -o-linear-gradient(top, rgba(238,95,91,1) 0%,rgba(196,60,53,1) 100%); + background: -ms-linear-gradient(top, rgba(238,95,91,1) 0%,rgba(196,60,53,1) 100%); + background: linear-gradient(to bottom, rgba(238,95,91,1) 0%,rgba(196,60,53,1) 100%); + background-repeat: repeat-x; + border-color: #a93730 #a93730 #952f2a; + } +.member-button .join-text, +.follow-button .follow-text { + display:block; + } +.member-button.member .join-text, +.follow-button.following .follow-text { + display:none; + } +.member-button.member .ismember-text, +.follow-button.following .following-text { + display:block; + } +.member-button.member:hover .ismember-text, +.follow-button.following:hover .following-text { + display:none; + } +.member-button.member:hover .leave-text, +.follow-button.following:hover .unfollow-text { + display:block; + } + + +/* SPINNER */ + +.spinner-wrap { + position: fixed; + top: 29px; + left:50%; + margin-left:-10px; + z-index:10000; +} +.spinner { + height: 20px; width: 20px; + margin: -20px auto 0; + animation: spin 1s steps(12, end) infinite; + -moz-animation: spin 1s steps(12, end) infinite; + -webkit-animation: spin 1s steps(12, end) infinite; +} + +.spinner i { + height: 5px; width: 2px; + margin-left: -1px; + display: block; + + position: absolute; + left: 50%; + + transform-origin: center 10px; + -webkit-transform-origin: center 10px; + -moz-transform-origin: center 10px; + + background: #fff; + border-radius: 2px; +} + +.spinner i:nth-child(1) { opacity: 0.08; } +.spinner i:nth-child(2) { transform: rotate(30deg); transform: rotate(30deg); transform: rotate(30deg); opacity: 0.167; } +.spinner i:nth-child(3) { transform: rotate(60deg); -moz-transform: rotate(60deg); -webkit-transform: rotate(60deg); opacity: 0.25; } +.spinner i:nth-child(4) { transform: rotate(90deg); -moz-transform: rotate(90deg); -webkit-transform: rotate(90deg); opacity: 0.33; } +.spinner i:nth-child(5) { transform: rotate(120deg); -moz-transform: rotate(120deg); -webkit-transform: rotate(120deg); opacity: 0.4167; } +.spinner i:nth-child(6) { transform: rotate(150deg); -moz-transform: rotate(150deg); -webkit-transform: rotate(150deg); opacity: 0.5; } +.spinner i:nth-child(7) { transform: rotate(180deg); -moz-transform: rotate(180deg); -webkit-transform: rotate(180deg); opacity: 0.583; } +.spinner i:nth-child(8) { transform: rotate(210deg); -moz-transform: rotate(210deg); -webkit-transform: rotate(210deg); opacity: 0.67; } +.spinner i:nth-child(9) { transform: rotate(240deg); -moz-transform: rotate(240deg); -webkit-transform: rotate(240deg); opacity: 0.75; } +.spinner i:nth-child(10) { transform: rotate(270deg); -moz-transform: rotate(270deg); -webkit-transform: rotate(270deg); opacity: 0.833; } +.spinner i:nth-child(11) { transform: rotate(300deg); -moz-transform: rotate(300deg); -webkit-transform: rotate(300deg); opacity: 0.9167; } +.spinner i:nth-child(12) { transform: rotate(330deg); -moz-transform: rotate(330deg); -webkit-transform: rotate(330deg); opacity: 1; } + +@keyframes spin { + from { transform: rotate(0deg); -moz-transform: rotate(0deg); -webkit-transform: rotate(0deg); } + to { transform: rotate(360deg); -moz-transform: rotate(360deg); -webkit-transform: rotate(360deg); } +} + +@-webkit-keyframes spin { + from { transform: rotate(0deg); -moz-transform: rotate(0deg); -webkit-transform: rotate(0deg); } + to { transform: rotate(360deg); -moz-transform: rotate(360deg); -webkit-transform: rotate(360deg); } +} + +@-moz-keyframes spin { + from { transform: rotate(0deg); -moz-transform: rotate(0deg); -webkit-transform: rotate(0deg); } + to { transform: rotate(360deg); -moz-transform: rotate(360deg); -webkit-transform: rotate(360deg); } +} + + +/* popups +------------*/ + +.modal-container { + bottom: 0; + display: block; + left: 0; + overflow-x: hidden; + overflow-y: auto; + position: fixed; + right: 0; + top: 0; + z-index: 5000; + background: none repeat scroll 0 0 rgba(0, 0, 0, 0.5); + } +.modal-draggable { + margin: 0; + padding-bottom: 30px; + position: relative; + top: 30%; + left: 50%; + width: 520px; + z-index: 6000; + } +.modal-content { + background-clip: padding-box; + background-color: #FFFFFF; + border-radius: 6px 6px 6px 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 1px 0 rgba(255, 255, 255, 0.5) inset; + position: relative; + } +.modal-close { + margin: 0; + border: 0 none; + cursor: pointer; + padding: 11px 12px; + background-color: transparent; + cursor: pointer; + padding: 11px 12px; + border-left: 1px solid #DDDDDD; + border-radius: 0 6px 0 0; + box-shadow: 1px 0 0 rgba(255, 255, 255, 0.5) inset; + position: absolute; + right: 0; + top: 0; + } +.modal-close:hover { + background-color:#eee; + } +.modal-close .icon { + cursor: pointer; + background-repeat: no-repeat; + display: inline-block; + vertical-align: text-top; + height: 12px; + width: 10px; + background-position: -60px -510px; + float: left; + } +.modal-close:hover .icon { + background-position: -80px -510px; + } +.modal-header { + padding: 15px; + background-color: #ECECEC; + background-image: -moz-linear-gradient(center top , #F5F5F5 0px, #ECECEC 100%); + background-repeat: repeat-x; + border-bottom: 1px solid #DDDDDD; + border-radius: 6px 6px 0 0; + box-shadow: 0 1px 0 #FFFFFF inset; + cursor: move; + padding-bottom: 8px; + padding-top: 8px; + } +.modal-header h3 { + margin: 0; + text-rendering: optimizelegibility; + font-size: 14px; + color: #555555; + font-size: 14px; + font-weight: bold; + line-height: 18px; + margin: 0 auto; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + text-shadow: 0 1px 0 #FFFFFF; + white-space: nowrap; + width: 75%; + } +.modal-body { + padding: 0; + text-align: left; + overflow:hidden; + } +.modal-footer { + padding: 15px; + text-align: left; + border-top: 1px solid #DDDDDD; + content: " "; + clear: both; + } +.modal-footer .queet { + border-bottom:0 none; + cursor:auto; + } +.modal-footer .queet:hover { + background-color:#fff; + } +.modal-footer .queet-content { + padding:0; + cursor:auto; + } +.modal-footer .queet-text { + cursor:auto; + } +.modal-body .inline-reply-queetbox { + padding-left:12px; + } +body.rtl .modal-body .inline-reply-queetbox { + direction:rtl; + text-align:right; + } +.modal-body .inline-reply-queetbox .queet-box-template { + width:478px; + } +.modal-body .queet { + background-color:#f5f5f5; + } +.modal-footer div.right { + text-align:right; + } +body.rtl .modal-footer div.right { + text-align:left; + direction:rtl; + } +.modal-footer button { + border: 1px solid #CCCCCC; + border-radius: 4px 4px 4px 4px; + color: #333333; + cursor: pointer; + display: inline-block; + font-size: 13px; + font-weight: bold; + line-height: 18px; + padding: 5px 10px; + position: relative; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); + background-color: #DDDDDD; + background: -moz-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(221,221,221,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,1)), color-stop(100%,rgba(221,221,221,1))); + background: -webkit-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(221,221,221,1) 100%); + background: -o-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(221,221,221,1) 100%); + background: -ms-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(221,221,221,1) 100%); + background: linear-gradient(to bottom, rgba(255,255,255,1) 0%,rgba(221,221,221,1) 100%); + background-repeat: repeat-x; + font-family: "Helvetica Neue",Arial,sans-serif; + font-weight:700; + margin-left: 10px; + } +.modal-footer button:hover { + background-color: #D8D8D8; + background: -moz-linear-gradient(top, rgba(248,248,248,1) 0%, rgba(216,216,216,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(248,248,248,1)), color-stop(100%,rgba(216,216,216,1))); + background: -webkit-linear-gradient(top, rgba(248,248,248,1) 0%,rgba(216,216,216,1) 100%); + background: -o-linear-gradient(top, rgba(248,248,248,1) 0%,rgba(216,216,216,1) 100%); + background: -ms-linear-gradient(top, rgba(248,248,248,1) 0%,rgba(216,216,216,1) 100%); + background: linear-gradient(to bottom, rgba(248,248,248,1) 0%,rgba(216,216,216,1) 100%); + border-color: #BBBBBB; + text-decoration: none; + } +.modal-footer button.primary { + background-color: #019AD2; + background: -moz-linear-gradient(top, rgba(51,188,239,1) 0%, rgba(1,154,210,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(51,188,239,1)), color-stop(100%,rgba(1,154,210,1))); + background: -webkit-linear-gradient(top, rgba(51,188,239,1) 0%,rgba(1,154,210,1) 100%); + background: -o-linear-gradient(top, rgba(51,188,239,1) 0%,rgba(1,154,210,1) 100%); + background: -ms-linear-gradient(top, rgba(51,188,239,1) 0%,rgba(1,154,210,1) 100%); + background: linear-gradient(to bottom, rgba(51,188,239,1) 0%,rgba(1,154,210,1) 100%); + background-repeat: repeat-x; + border-color: #057ED0; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1) inset; + color: #FFFFFF; + text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25); + } +.modal-footer button.primary:hover { + background-color: #0271BF; + background: -moz-linear-gradient(top, rgba(45,173,220,1) 0%, rgba(2,113,191,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(45,173,220,1)), color-stop(100%,rgba(2,113,191,1))); + background: -webkit-linear-gradient(top, rgba(45,173,220,1) 0%,rgba(2,113,191,1) 100%); + background: -o-linear-gradient(top, rgba(45,173,220,1) 0%,rgba(2,113,191,1) 100%); + background: -ms-linear-gradient(top, rgba(45,173,220,1) 0%,rgba(2,113,191,1) 100%); + background: linear-gradient(to bottom, rgba(45,173,220,1) 0%,rgba(2,113,191,1) 100%); + background-repeat: repeat-x; + border-color: #096EB3; + color: #FFFFFF; + } +body.rtl .modal-footer button { + font-family:Tahoma,Arial,sans-serif; + margin-left: 10px; + } + +.thread-container { + margin:10px 20px 100px 20px; + } +.thread-container .stream-item { + margin:0; + margin-bottom:1px; + border-radius:3px; + } +.thread-container .stream-item .stream-item-footer { + height: 20px; + } + + + +/* RTL +--------- */ + +body.rtl .profile-card .user-actions { + float: left; + } +body.rtl .profile-banner-footer ul.stats { + float: right; + } + +.queet-text .vcard .mention, +.queet-text .vcard .group, +.queet-text .tag a { + direction:ltr; + } +.queet-text .vcard .mention, +.queet-text .vcard .group { + unicode-bidi:bidi-override; + } +.queet.rtl .queet-text .tag a { + direction:rtl; + } +.queet-text .vcard .mention:before { + content:"@"; + } +.queet.rtl .queet-text .vcard .mention:before { + content: ""; + } +.queet.rtl .queet-text .vcard .mention:after { + content: "@"; + } +.queet-text .vcard .group:before { + content:"!"; + } +.queet.rtl .queet-text .vcard .group:before { + content: ""; + } +.queet.rtl .queet-text .vcard .group:after { + content: "!"; + } +.queet-text .tag a:before { + content:"#"; + } + + +body.rtl table.password-signin { + direction:rtl; + } + +body.rtl .flex-table-primary { + padding-left: 5px; + padding-right: 0; + } + +body.rtl #search { + float:left; + } +body.rtl .search-icon { + left:2px; + right:auto; + } +body.rtl #search-query { + padding: 6px 12px 6px 27px; + direction:rtl; + } +body.rtl .profile-card { + float:left; + margin-left:-1px; + margin-right:auto; + } + +body.rtl #user-body { + direction:rtl; + } + +body.rtl #remember-forgot, +body.rtl #login-content input#username, +body.rtl #login-content input#password, +body.rtl .queet-box, +body.rtl #user-header, +body.rtl .queet-box-template, +body.rtl #new-queets-bar, +body.rtl .queet-actions, +body.rtl .longdate { + direction:rtl; + } +body.rtl #queet-box[contenteditable="true"]:focus { + text-align:right; + direction:rtl; + } +body.rtl #queet-button, +body.rtl .queet-button { + direction: rtl; + float:left; + } +body.rtl #queet-counter, +body.rtl .queet-counter { + margin-right:0; + margin-left:5px; + text-align: left; + } +body.rtl #user-body #user-queets, +body.rtl #user-body #user-following, +body.rtl #user-body #user-groups, +body.rtl #user-body #user-followers, +body.rtl .profile-banner-footer ul.stats li { + float:right; + direction: rtl; + } +body.rtl .profile-banner-footer ul.stats li:first-child a, +body.rtl #user-body #user-queets, +body.rtl #user-body #user-following, +body.rtl #user-body #user-followers { + border-left:1px solid #E8E8E8; + } +body.rtl .profile-banner-footer ul.stats li:last-child a { + border-left:0 none; + } +body.rtl #user-body #user-groups { + border:0 none; + } +body.rtl #user-avatar { + left:auto; + right:12px; + } +body.rtl #user-name, +body.rtl #user-profile-link a { + margin-right: 42px; + margin-left: 20px; + } +body.rtl #user-container { + float: right; + } +body.rtl #feed { + float: left; + } +body.rtl #birds-top { + right:auto; + left:-3px; + } +body.rtl .language-dropdown { + float:left; + direction: rtl; + margin-left: 0; + margin-right: 6px; + } +body.rtl .quitter-settings.dropdown-menu { + left:auto; + right:50%; + margin-right:-410px; + margin-left:0; + } + +body.rtl #logo { + left:auto; + right:50%; + margin-right: -420px; + margin-left:auto; + } +body.rtl .dropdown-caret.right { + right: auto; + left: 10px; + } +body.rtl .dropdown-menu { + float: left; + right: auto; + left:0; + } +body.rtl .dropdown-toggle .caret { + margin-right: 2px; + margin-left:0; + } + +body.rtl #logolink .dropdown-toggle { + left: auto; + right:50%; + margin-left:0; + margin-right: -343px; + direction:rtl; + } +body.rtl #logolink .nav-session { + margin-left:6px; + margin-right:0; + } +body.rtl #logolink .caret { + display:block; + position:absolute; + margin-left:0; + margin-right:25px; + top:13px; + } +body.rtl .menu-container div .chev-right { + right:auto; + left:12px; + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); + } +body.rtl .menu-container div .close-right { + margin-right:5px; + } +body.rtl .menu-container div { + direction:rtl; + } + +body.rtl #feed-header-inner h2 { + direction: rtl; + } + +body.rtl, +body.rtl label, +body.rtl .label, +body.rtl input, +body.rtl textarea, +body.rtl select, +body.rtl button, +body.rtl span, +body.rtl span.screen-name, +body.rtl strong.name, +body.rtl a, +body.rtl p, +body.rtl div.queet-text, +body.rtl .dropdown-toggle, +body.rtl #remember-forgot, +body.rtl .queet-box, +body.rtl #new-queets-bar, +body.rtl #user-name, +body.rtl #feed-header-inner h2 { + font-family: Tahoma,Arial,sans-serif !important; + } + + +/* =Responsive Structure +----------------------------------------------- */ + +@media (max-width: 1280px) { + body { + background-size:auto auto; + } + } + +@media (max-width: 1050px) { + .language-dropdown { + position:fixed; + right:100px; + } + body.rtl .language-dropdown { + left:100px; + right:auto; + } + } + +@media (max-width: 866px) { + + #page-container { + width: 520px; + margin-left: -275px; + } + #user-container { + width:522px; + } + #feed { + float:left; + width:524px; + } + #logo { + margin-left: -265px; + } + body.rtl #logo { + margin-right:-265px; + } + #logolink .dropdown-toggle { + margin-left: -187px; + } + body.rtl #logolink .dropdown-toggle { + margin-right: -189px; + margin-left:0; + } + .quitter-settings.dropdown-menu { + margin-left:-260px; + } + body.rtl .quitter-settings.dropdown-menu { + margin-left:0; + margin-right:-260px; + } + + #queet-box, + #queet-box[contenteditable="true"] { + width:478px; + } + } +@media (max-width: 566px) { + + body { + overflow-x:hidden; + } + + #logo{ + margin-left: -48.5%; + } + #logolink .dropdown-toggle { + left:0; + padding-left:5px; + margin-left: 95px; + } + .quitter-settings.dropdown-menu { + left:0; + margin-left:20px; + } + body.rtl .quitter-settings.dropdown-menu { + left:auto; + right:0; + margin-left:0; + margin-right:15px; + } + body.rtl #logolink .dropdown-toggle { + left:auto; + right:0; + padding-left:0; + margin-left: 0; + padding-right:5px; + margin-right: 92px; + } + body.rtl #logo{ + margin-right: -48.5%; + margin-left: auto; + } + #page-container { + width: 200%; + margin-left: -100%; + padding:0; + position:relative; + } + #user-container { + width:95%; + margin-left:2.5%; + margin-top:14px; + } + body.rtl #user-container { + margin-right:2.5%; + margin-left:0; + } + #feed { + width:95%; + margin-left:2.5%; + } + #user-footer { + padding-left:2.5%; + } + + .modal-draggable { + width:95%; + } + #queet-box, + #queet-box[contenteditable="true"], + .queet-box-template, + .modal-body .inline-reply-queetbox .queet-box-template { + width:95%; + } + + .stream-item.activity .queet-text { + width:95%; + } + + .stream-item.expanded .media img { + max-width: 100%; + } + + } + +@media (max-width: 466px) { + #logo { + display:none; + } + #logolink .dropdown-toggle { + margin-left:5px; + } + body.rtl #logolink .dropdown-toggle { + margin-right:5px; + } + + .quitter-settings.dropdown-menu { + left:0; + margin-left:20px; + } + .quitter-settings.dropdown-menu .dropdown-caret { + right: auto; + left: 10px; + } + body.rtl .quitter-settings.dropdown-menu { + left:auto; + right:0; + margin-left:0; + margin-right:20px; + } + body.rtl .quitter-settings.dropdown-menu .dropdown-caret { + left: auto; + right: 10px; + } + + body, .topbar .global-nav { + background-image:none !important; + background-color:#222; + } + #birds-top { + width:80px; + } + #user-footer, #feed-header-inner { + border-radius:0 0 0 0; + } + #user-container { + width:100%; + margin-left:0%; + margin-top:-3px; + margin-bottom:-3px; + } + body.rtl #user-container { + margin-right:-1px; + margin-left:0%; + } + #feed { + width:101%; + margin-left:-1px; + } + #feed-header{ + } + + ul.queet-actions li .with-icn b { + display:none; + } + .stream-item-footer .with-icn .requeet-text { + font-size:0; + } + .stream-item-footer .with-icn .requeet-text a { + font-size:12px; + } + } diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..172b729 Binary files /dev/null and b/favicon.ico differ diff --git a/img/birds.png b/img/birds.png new file mode 100644 index 0000000..932841d Binary files /dev/null and b/img/birds.png differ diff --git a/img/birds_rtl.png b/img/birds_rtl.png new file mode 100644 index 0000000..3b81dcc Binary files /dev/null and b/img/birds_rtl.png differ diff --git a/img/button_birds.png b/img/button_birds.png new file mode 100644 index 0000000..6a9e219 Binary files /dev/null and b/img/button_birds.png differ diff --git a/img/ekan4.jpg b/img/ekan4.jpg new file mode 100644 index 0000000..1814c86 Binary files /dev/null and b/img/ekan4.jpg differ diff --git a/img/logo.png b/img/logo.png new file mode 100644 index 0000000..b90e5b3 Binary files /dev/null and b/img/logo.png differ diff --git a/img/sprite.png b/img/sprite.png new file mode 100644 index 0000000..b68fdbb Binary files /dev/null and b/img/sprite.png differ diff --git a/img/sprite_bgs.png b/img/sprite_bgs.png new file mode 100644 index 0000000..0fea51d Binary files /dev/null and b/img/sprite_bgs.png differ diff --git a/index.php b/index.php new file mode 100644 index 0000000..8737892 --- /dev/null +++ b/index.php @@ -0,0 +1,176 @@ + + + + <?php print $sitetitle; ?> + + + + + + + +
+ + + + + + + +
+
+ +
+
+
+

+
+
+ +
+
+ + +
+ + + + + + + + + + \ No newline at end of file diff --git a/js/ajax-functions-1.js b/js/ajax-functions-1.js new file mode 100644 index 0000000..7cf0380 --- /dev/null +++ b/js/ajax-functions-1.js @@ -0,0 +1,397 @@ + + /* · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · + · · + · · + · Q V I T T E R · + · · + · http://github.com/hannesmannerheim/qvitter · + · · + · · + · \\\\_\ · + · \\) \____) · + · · + · · + · · + · Qvitter is free software: you can redistribute it and / or modify it · + · under the terms of the GNU Affero General Public License as published by · + · the Free Software Foundation, either version three of the License or (at · + · your option) any later version. · + · · + · Qvitter is distributed in hope that it will be useful but WITHOUT ANY · + · WARRANTY; without even the implied warranty of MERCHANTABILTY or FITNESS · + · FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for · + · more details. · + · · + · You should have received a copy of the GNU Affero General Public License · + · along with Qvitter. If not, see . · + · · + · Contact h@nnesmannerhe.im if you have any questions. · + · · + · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · */ + + +/* · + · + · Check login credentials with http basic auth + · + · @param username: users screen name + · @param password: users password + · @param actionOnSuccess: callback function on log in success + · + · · · · · · · · · */ + +function checkLogin(username,password,actionOnSuccess) { + $.ajax({ url: window.fullUrlToThisQvitterApp + 'API.php', + type: 'POST', + data: { + getRequest: "account/verify_credentials.json", + username: username, + password: password + }, + dataType: 'json', + success: function(data) { + if(typeof data.error == 'undefined') { + actionOnSuccess(data); + } + else { + alert(data.error); + remove_spinner(); + $('#submit-login').removeAttr('disabled'); + } + } + }); + } + + + +/* · + · + · Generic API GET request + · + · @param stream: any api get-request e.g. 'statuses/favs/111111.json' + · @param actionOnSuccess: callback function + · + · · · · · · · · · · · · · */ + +function getFromAPI(stream, actionOnSuccess) { + + // request without username/password + if(typeof window.loginUsername == 'undefined') { + $.ajax({ url: window.fullUrlToThisQvitterApp + 'API.php', + type: "POST", + data: { + getRequest: stream + }, + dataType: 'json', + success: function(data) { + actionOnSuccess(data); + }, + error: function(data) { + actionOnSuccess(false); + console.log(data); + remove_spinner(); + } + }); + } + // with username/password if set + else { + $.ajax({ url: window.fullUrlToThisQvitterApp + 'API.php', + type: "POST", + data: { + getRequest: stream, + username: window.loginUsername, + password: window.loginPassword + }, + dataType: 'json', + success: function(data) { + actionOnSuccess(data); + }, + error: function(data) { + actionOnSuccess(false); + console.log(data); + remove_spinner(); + } + }); + } + } + + + +/* · + · + · Post queet + · + · @param queetText_txt: the text to post + · @param actionOnSuccess: callback function, false on error, data on success + · + · · · · · · · · · · · · · */ + +function postQueetToAPI(queetText_txt, actionOnSuccess) { + $.ajax({ url: window.fullUrlToThisQvitterApp + 'API.php', + type: "POST", + data: { + postRequest: 'statuses/update.json', + status: queetText_txt, + source: 'Qvitter', + username: window.loginUsername, + password: window.loginPassword + }, + dataType: "json", + error: function(data){ actionOnSuccess(false); console.log(data); }, + success: function(data) { actionOnSuccess(data);} + }); + } + + +/* · + · + · Post follow or unfollow user request + · + · @param followOrUnfollow: either 'follow' or 'unfollow' + · @param user_id: the user id of the user we want to follow + · @param actionOnSuccess: callback function, false on error, data on success + · + · · · · · · · · · · · · · */ + +function APIFollowOrUnfollowUser(followOrUnfollow,user_id,this_element,actionOnSuccess) { + + if(followOrUnfollow == 'follow') { + var postRequest = 'friendships/create.json'; + } + else if (followOrUnfollow == 'unfollow') { + var postRequest = 'friendships/destroy.json'; + } + + $.ajax({ url: window.fullUrlToThisQvitterApp + 'API.php', + type: "POST", + data: { + postRequest: postRequest, + user_id: user_id, + username: window.loginUsername, + password: window.loginPassword + }, + dataType:"json", + error: function(data){ actionOnSuccess(false,this_element); console.log(data); }, + success: function(data) { actionOnSuccess(data,this_element);} + }); + } + + +/* · + · + · Post join or leave group request + · + · @param joinOrLeave: either 'join' or 'leave' + · @param group_id: group's id + · @param actionOnSuccess: callback function, false on error, data on success + · + · · · · · · · · · · · · · */ + +function APIJoinOrLeaveGroup(joinOrLeave,group_id,this_element,actionOnSuccess) { + $.ajax({ url: window.fullUrlToThisQvitterApp + 'API.php', + type: "POST", + data: { + postRequest: 'statusnet/groups/' + joinOrLeave + '.json', + id: group_id, + username: window.loginUsername, + password: window.loginPassword + }, + dataType:"json", + error: function(data){ actionOnSuccess(false,this_element); console.log(data); }, + success: function(data) { actionOnSuccess(data,this_element);} + }); + } + + +/* · + · + · Post reply + · + · @param queetText_txt: the text to post + · @param in_reply_to_status_id: the local id for the queet to reply to + · @param actionOnSuccess: callback function, false on error, data on success + · + · · · · · · · · · · · · · */ + +function postReplyToAPI(queetText_txt, in_reply_to_status_id, actionOnSuccess) { + $.ajax({ url: window.fullUrlToThisQvitterApp + 'API.php', + type: "POST", + data: { + postRequest: 'statuses/update.json', + status: queetText_txt, + source: 'Qvitter', + username: window.loginUsername, + password: window.loginPassword, + in_reply_to_status_id: in_reply_to_status_id + }, + dataType:"json", + error: function(data){ actionOnSuccess(false); console.log(data); }, + success: function(data) { actionOnSuccess(data);} + }); + } + + + + +/* · + · + · Generic POST-action + · + · @param action: the api action, e.q. 'statuses/retweet/1.json' + · @param actionOnSuccess: callback function, false on error, data on success + · + · · · · · · · · · · · · · */ + +function postActionToAPI(action, actionOnSuccess) { + $.ajax({ url: window.fullUrlToThisQvitterApp + 'API.php', + type: "POST", + data: { + postRequest: action, + source: 'Qvitter', + username: window.loginUsername, + password: window.loginPassword + }, + dataType:"json", + error: function(data){ actionOnSuccess(false); console.log(data); }, + success: function(data) { actionOnSuccess(data);} + }); + } + + +/* · + · + · Generic POST-action + · + · @param action: the api action, e.q. 'statuses/retweet/1.json' + · @param actionOnSuccess: callback function, false on error, data on success + · + · · · · · · · · · · · · · */ + +function postActionToAPI(action, actionOnSuccess) { + $.ajax({ url: window.fullUrlToThisQvitterApp + 'API.php', + type: "POST", + data: { + postRequest: action, + source: 'Qvitter', + username: window.loginUsername, + password: window.loginPassword + }, + dataType:"json", + error: function(data){ actionOnSuccess(false); console.log(data); }, + success: function(data) { actionOnSuccess(data);} + }); + } + + +/* · + · + · Delete requeet + · + · @param this_stream_item: jQuery object for stream-item + · @param this_action: JQuery object for the requeet-button + · @param my_rq_id: the id for the requeet + · + · · · · · · · · · */ + +function unRequeet(this_stream_item, this_action, my_rq_id) { + this_action.children('.with-icn').removeClass('done'); + this_action.find('.with-icn b').html(window.sL.requeetVerb); + this_stream_item.removeClass('requeeted'); + + // post unrequeet + postActionToAPI('statuses/destroy/' + my_rq_id + '.json', function(data) { + if(data) { + remove_spinner(); + this_stream_item.removeAttr('data-requeeted-by-me-id'); + } + else { + remove_spinner(); + this_action.children('.with-icn').addClass('done'); + this_action.find('.with-icn b').html(window.sL.requeetedVerb); + this_stream_item.addClass('requeeted'); + } + }); + } + + + +/* · + · + · Gets favs or requeets for a queet from api + · + · @param apiaction: i.e. 'favs' or 'requeets' + · @param qid: the queet id + · @param actionOnSuccess: callback function + · + · · · · · · · · · */ + +function getFavsOrRequeetsForQueet(apiaction,qid,actionOnSuccess) { + if(apiaction=="requeets") { apiaction="retweets"; } // we might mix this up... + $.ajax({ url: window.fullUrlToThisQvitterApp + 'API.php', + type: "POST", + data: { + getRequest: "statuses/" + apiaction + "/" + qid + ".json", + username: window.loginUsername, + password: window.loginPassword + }, + dataType: 'json', + success: function(data) { + if(data.length > 0) { + actionOnSuccess(data); + } + else { + actionOnSuccess(false); + } + }, + error: function(data) { + remove_spinner(); + console.log(data); + } + }); + } + +/* · + · + · Shorten urls in box + · + · @param apiaction: i.e. 'favs' or 'requeets' + · @param qid: the queet id + · @param actionOnSuccess: callback function + · + · params included to pass along to countCharsInQueetBox + · + · · · · · · · · · · · · · */ + +function shortenUrlsInBox(box,cnt,btn) { + // wrap urls +// var allurls = findUrls(box.html().replace(/&/gi,'&').replace(/ /gi,' ')); +// $.each(allurls,function(key,obj){ +// if(obj.substring(0,15) != 'http://qttr.at/' && obj.length > 20) { // don't shorten if link is qttr.at or very short already +// box.html(box.html().replace(/&/gi,'&').replace(obj,'' + obj + '')); +// placeCaretAtEnd(document.getElementById(box.attr('id'))); +// } +// }); +// +// // shorten urls vith qttr.at +// $.each(box.find('a.shortening'),function(key,obj){ +// display_spinner(); +// var urlEncodedUrl = encodeURIComponent($(obj).html().replace(/&/gi,'&')); +// $.ajax({ url: "http://qttr.at/yourls-api.php?format=jsonp&action=shorturl&signature=b6afeec983&url=" + urlEncodedUrl, type: "GET", dataType: "jsonp", success: function(data) { +// if(typeof data.shorturl != 'undefined') { +// $(obj).before(data.shorturl); +// } +// else { +// $(obj).before($(obj).html()); +// } +// $(obj).remove(); +// remove_spinner(); +// placeCaretAtEnd(document.getElementById(box.attr('id'))); +// countCharsInQueetBox(box,cnt,btn); +// }}); +// }); + } + diff --git a/js/dom-functions-1.js b/js/dom-functions-1.js new file mode 100644 index 0000000..4dea14d --- /dev/null +++ b/js/dom-functions-1.js @@ -0,0 +1,1381 @@ + + /* · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · + · · + · · + · Q V I T T E R · + · · + · http://github.com/hannesmannerheim/qvitter · + · · + · · + · \\\\_\ · + · \\) \____) · + · · + · · + · · + · Qvitter is free software: you can redistribute it and / or modify it · + · under the terms of the GNU Affero General Public License as published by · + · the Free Software Foundation, either version three of the License or (at · + · your option) any later version. · + · · + · Qvitter is distributed in hope that it will be useful but WITHOUT ANY · + · WARRANTY; without even the implied warranty of MERCHANTABILTY or FITNESS · + · FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for · + · more details. · + · · + · You should have received a copy of the GNU Affero General Public License · + · along with Qvitter. If not, see . · + · · + · Contact h@nnesmannerhe.im if you have any questions. · + · · + · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · */ + + +/* · + · + · Main stream item builder function + · + · Not used yet, I was only thinking that the addToFeed()-function + · hidden far down in this file should be restructured into something + · in this direction. + · + · @param streamItemObj: object with all necessary data to build stream + · + · @return the html of the stream item + · + · · · · · · · · · */ + +function buildStreamItem(streamItemObj) { + + // make a jquery base element + var streamItemContainer = $('
').append('
'); + var streamItem = streamItemContainer.children('.stream-item'); + + // give it an id + streamItem.attr('id',streamItemObj.id); + + // give it classes + $.each(streamItemObj.classes, function(k,o){ + streamItem.addClass(o); + }); + + return streamItemContainer.html(); + } + + + + +/* · + · + · Show favs or requeets in queet element + · + · @param q: queet jQuery object + · @param users: object with users that has faved or requeeted + · @param type: fav or requeet + · + · · · · · · · · · */ + +function showFavsOrRequeetsInQueet(q,users,type) { + + // label maybe plural + if(users.length == 1) { + if(type=='favs') { var label = window.sL.favoriteNoun; } + else if(type=='requeets') { var label = window.sL.requeetNoun; } + } + else if(users.length > 1) { + if(type=='favs') { var label = window.sL.favoritesNoun; } + else if(type=='requeets') { var label = window.sL.requeetsNoun; } + } + + var html = '
  • ' + users.length + ' ' + label + '
  • '; + + // add fav and rq-container if it doesn't exist + if(!q.find('ul.stats').length > 0) { + q.find('.queet-stats-container').prepend('
    '); + } + + // requeets always first + if(type=='favs') { + q.find('li.avatar-row').before(html.replace('x-count','fav-count')); + } + else if(type=='requeets') { + q.find('ul.stats').prepend(html.replace('x-count','rq-count')); + } + + // show max seven avatars + var avatarnum = q.find('.avatar').length; + if(avatarnum < 7) { + $.each(users,function(){ + + // api return little different objects for fav and rq + var userObj = new Object(); + if(type=='favs') { + userObj = this; + } + else if(type=='requeets') { + userObj.fullname = this.user.name; + userObj.user_id = this.user.id; + userObj.profileurl = this.user.statusnet_profile_url; + userObj.avatarurl = this.user.profile_image_url; + } + + // no dupes + if(q.children('.queet').find('#av-' + userObj.user_id).length==0) { + q.find('.avatar-row').append('' + userObj.fullname + ''); + avatarnum++; + } + return (avatarnum < 8); + }); + } + } + + + +/* · + · + · Adds a profile card before feed element, with data from the first object in the included object + · + · @param data: an object with one or more queet objects + · + · · · · · · · · · */ + +function profileCardFromFirstObject(data,screen_name) { + + var first = new Object(); + for (var i in data) { + if (data.hasOwnProperty(i) && typeof(i) !== 'function') { + first = data[i]; + break; + } + } + + if(typeof first.user != 'undefined') { + + // we don't want to print 'null' + first.user.name = first.user.name || ''; + first.user.profile_image_url = first.user.profile_image_url || ''; + first.user.profile_image_url_profile_size = first.user.profile_image_url_profile_size || ''; + first.user.profile_image_url_original = first.user.profile_image_url_original || ''; + first.user.screen_name = first.user.screen_name || ''; + first.user.description = first.user.description || ''; + first.user.location = first.user.location || ''; + first.user.url = first.user.url || ''; + first.user.statusnet_profile_url = first.user.statusnet_profile_url || ''; + first.user.statuses_count = first.user.statuses_count || 0; + first.user.followers_count = first.user.followers_count || 0; + first.user.friends_count = first.user.friends_count || 0; + + // show user actions if logged in + var followingClass = ''; + if(first.user.following) { + followingClass = 'following'; + } + var followButton = ''; + if(typeof window.loginUsername != 'undefined' && window.myUserID != first.user.id) { + var followButton = ''; + } + + $('#feed').before(''); + } + + // if user hasn't queeted or if we're not allowed to read their queets + else { + getFromAPI('users/show/' + screen_name + '.json', function(data){ if(data){ + data.name = data.name || ''; + data.profile_image_url = data.profile_image_url || ''; + data.profile_image_url_profile_size = data.profile_image_url_profile_size || ''; + data.profile_image_url_original = data.profile_image_url_original || ''; + data.screen_name = data.screen_name || ''; + data.description = data.description || ''; + data.location = data.location || ''; + data.url = data.url || ''; + data.statusnet_profile_url = data.statusnet_profile_url || ''; + data.statuses_count = data.statuses_count || 0; + data.followers_count = data.followers_count || 0; + data.groups_count = data.groups_count || 0; + data.friends_count = data.friends_count || 0; + + // show user actions if logged in + var followingClass = ''; + if(data.following) { + followingClass = 'following'; + } + var followButton = ''; + if(typeof window.loginUsername != 'undefined' && window.myUserID != data.id) { + var followButton = ''; + } + + $('#feed').before(''); + }}); + } + } + + + + +/* · + · + · Adds a group profile card before feed element + · + · @param data: an object with one or more queet objects + · + · · · · · · · · · */ + +function groupProfileCard(groupAlias) { + getFromAPI('statusnet/groups/show/' + groupAlias + '.json', function(data){ if(data){ + + data.nickname = data.nickname || ''; + data.fullname = data.fullname || ''; + data.stream_logo = data.stream_logo || 'http://quitter.se/theme/quitter-theme2/default-avatar-stream.png'; + data.homepage_logo = data.homepage_logo || 'http://quitter.se/theme/quitter-theme2/default-avatar-profile.png'; + data.original_logo = data.original_logo || 'http://quitter.se/theme/quitter-theme2/default-avatar-profile.png'; + data.description = data.description || ''; + data.homepage = data.homepage || ''; + data.url = data.url || ''; + data.member_count = data.member_count || 0; + data.admin_count = data.admin_count || 0; + + // show user actions if logged in + var memberClass = ''; + if(data.member) { + memberClass = 'member'; + } + var memberButton = ''; + if(typeof window.loginUsername != 'undefined') { + var memberButton = ''; + } + + // add card to DOM + $('#feed').before(''); + }}); + } + + + +/* · + · + · Change stream + · + · @param stream: part of the url to the api (everything after api base url) + · @param actionOnSuccess: callback function on success + · + · · · · · · · · · */ + +function setNewCurrentStream(stream,actionOnSuccess,setLocation) { + + // don't do anything if this stream is already the current + if(window.currentStream == stream) { + return; + } + + // set location bar from stream + if(setLocation && window.useHistoryPushState) { + setUrlFromStream(stream); + } + + // halt interval that checks for new queets + window.clearInterval(checkForNewQueetsInterval); + + display_spinner(); + $(window).scrollTop(0); + $('#feed-body').removeAttr('data-search-page-number'); // null any searches + + // remember the most recent stream selection in global var + window.currentStream = stream; + + // if this is a @user stream, i.e. user's queets, user's followers, user's following, we set _queets_ as the default stream in the menu + if(stream.substring(0,36) == 'statuses/followers.json?screen_name=' + || stream.substring(0,34) == 'statuses/friends.json?screen_name=' + || stream.substring(0,39) == 'statusnet/groups/list.json?screen_name=' + || stream.substring(0,43) == 'statuses/friends_timeline.json?screen_name=' + || stream.substring(0,27) == 'favorites.json?screen_name=' + || stream.substring(0,35) == 'statuses/mentions.json?screen_name=' + || stream.substring(0,27) == 'statuses/user_timeline.json') { + var defaultStreamName = 'statuses/user_timeline' + stream.substring(stream.indexOf('.json')); + var streamHeader = '@' + stream.substring(stream.indexOf('=')+1); + } + // if this is a my user streams, i.e. my followers, my following + else if(stream == 'statuses/followers.json' + || stream == 'statuses/friends.json' + || stream == 'statusnet/groups/list.json') { + var defaultStreamName = stream; + var streamHeader = '@' + window.loginUsername; + } + // if this is one of the default streams, get header from DOM + else if(stream == 'statuses/friends_timeline.json' + || stream == 'statuses/mentions.json' + || stream == 'favorites.json' + || stream == 'statuses/public_timeline.json') { + var defaultStreamName = stream; + var streamHeader = $('.stream-selection[data-stream-name="' + stream + '"]').attr('data-stream-header'); + } + + // if this is a !group stream + else if(stream.substring(0,26) == 'statusnet/groups/timeline/') { + var defaultStreamName = stream; + var streamHeader = '!' + stream.substring(stream.indexOf('/timeline/')+10,stream.indexOf('.json')); + } + // if this is a #tag stream + else if(stream.substring(0,24) == 'statusnet/tags/timeline/') { + var defaultStreamName = stream; + var streamHeader = '#' + stream.substring(stream.indexOf('/timeline/')+10,stream.indexOf('.json')); + } + // if this is a search stream + else if(stream.substring(0,11) == 'search.json') { + var defaultStreamName = stream; + var streamHeader = window.sL.searchVerb + ': ' + decodeURIComponent(stream.substring(stream.indexOf('?q=')+3)); + } + + // set the h2 header in the feed + if(stream.substring(0,23) == 'statuses/followers.json') { + var h2FeedHeader = window.sL.followers; + } + else if(stream.substring(0,21) == 'statuses/friends.json') { + var h2FeedHeader = window.sL.following; + } + else if(stream.substring(0,40) == 'statuses/user_timeline.json?screen_name=') { + var h2FeedHeader = window.sL.notices + ''; + } + else if(stream.substring(0,35) == 'statuses/mentions.json?screen_name=') { + var h2FeedHeader = '' + window.sL.mentions + ''; + } + else if(stream.substring(0,27) == 'favorites.json?screen_name=') { + var h2FeedHeader = '' + window.sL.favoritesNoun; + } + else if(stream.substring(0,26) == 'statusnet/groups/list.json') { + var h2FeedHeader = window.sL.groups; + } + else if(stream.substring(0,43) == 'statuses/friends_timeline.json?screen_name=') { + var h2FeedHeader = '' + streamHeader + '/all'; // ugly rtl fix, sry, we should have translations for this stream header + } + else { + var h2FeedHeader = streamHeader; + } + + // fade out + $('#feed,.profile-card').animate({opacity:'0'},150,function(){ + // when fade out finishes, remove any profile cards + $('.profile-card').remove(); + $('#feed-header-inner h2').html(h2FeedHeader); + }); + + // add this stream to history menu if it doesn't exist there (but not if this is me or if we're not logged in) + if($('.stream-selection[data-stream-header="' + streamHeader + '"]').length==0 + && streamHeader != '@' + window.loginUsername + && typeof window.loginUsername != 'undefined') { + $('#history-container').append('
    ' + streamHeader + '
    '); + updateHistoryLocalStorage(); + } + + // mark this stream as current in menu + $('.stream-selection').removeClass('current'); + $('.stream-selection[data-stream-header="' + streamHeader + '"]').addClass('current'); + + // if this is user's user feed, i.e. followers etc, we want a profile card, which we need to get from user_timeline since the users/show api action is broken (auth doesn't work) + if(stream.substring(0,23) == 'statuses/followers.json' + || stream.substring(0,21) == 'statuses/friends.json' + || stream.substring(0,26) == 'statusnet/groups/list.json' + || stream.substring(0,35) == 'statuses/mentions.json?screen_name=' + || stream.substring(0,27) == 'favorites.json?screen_name=' + || stream.substring(0,43) == 'statuses/friends_timeline.json?screen_name=') { + getFromAPI(defaultStreamName + '&count=1', function(profile_data){ + if(profile_data) { + getFromAPI(stream, function(user_data){ + if(user_data) { + // while waiting for this data user might have changed stream, so only proceed if current stream still is this one + if(window.currentStream == stream) { + profileCardFromFirstObject(profile_data,window.loginUsername); // show profile card + checkForNewQueetsInterval=window.setInterval(function(){checkForNewQueets()},window.timeBetweenPolling); // start interval again + remove_spinner(); + $('#feed-body').html(''); // empty feed only now so the scrollers don't flicker on and off + addToFeed(user_data, false,'visible'); // add stream items to feed element + $('#feed').animate({opacity:'1'},150); // fade in + $('body').removeClass('loading-older');$('body').removeClass('loading-newer'); + actionOnSuccess(); // return + } + } + }); + } + }); + } + + // if this is a queet stream + else { + getFromAPI(stream, function(queet_data){ + if(queet_data) { + // while waiting for this data user might have changed stream, so only proceed if current stream still is this one + if(window.currentStream == stream) { + + // show profile card if this is a user's queet stream + if(stream.substring(0,27) == 'statuses/user_timeline.json') { + var thisUsersScreenName = stream.replace('statuses/user_timeline.json','').replace('?screen_name=','').replace('?id=','').replace('?user_id=',''); + profileCardFromFirstObject(queet_data,thisUsersScreenName); + } + // show group profile card if this is a group stream + else if(stream.substring(0,26) == 'statusnet/groups/timeline/') { + var thisGroupAlias = stream.replace('statusnet/groups/timeline/','').replace('.json',''); + groupProfileCard(thisGroupAlias); + } + + checkForNewQueetsInterval=window.setInterval(function(){checkForNewQueets()},window.timeBetweenPolling); // start interval again + remove_spinner(); + $('#feed-body').html(''); // empty feed only now so the scrollers don't flicker on and off + addToFeed(queet_data, false,'visible'); // add stream items to feed element + $('#feed').animate({opacity:'1'},150); // fade in + $('body').removeClass('loading-older');$('body').removeClass('loading-newer'); + actionOnSuccess(); // return + } + } + }); + } + } + + +/* · + · + · Sets the location bar in the browser to correspond with given stream + · + · @param stream: the stream, e.g. 'public_timeline.json' + · + · · · · · · · · · */ + +function setUrlFromStream(stream) { + + if(stream.substring(0,36) == 'statuses/followers.json?screen_name=') { + var screenName = stream.substring(stream.indexOf('=')+1); + history.pushState({strm:stream},'','/' + screenName + '/subscribers'); + } + else if(stream.substring(0,34) == 'statuses/friends.json?screen_name=') { + var screenName = stream.substring(stream.indexOf('=')+1); + history.pushState({strm:stream},'','/' + screenName + '/subscriptions'); + } + else if(stream.substring(0,35) == 'statuses/mentions.json?screen_name=') { + var screenName = stream.substring(stream.indexOf('=')+1); + history.pushState({strm:stream},'','/' + screenName + '/replies'); + } + else if(stream.substring(0,27) == 'favorites.json?screen_name=') { + var screenName = stream.substring(stream.indexOf('=')+1); + history.pushState({strm:stream},'','/' + screenName + '/favorites'); + } + else if(stream.substring(0,39) == 'statusnet/groups/list.json?screen_name=') { + var screenName = stream.substring(stream.indexOf('=')+1); + history.pushState({strm:stream},'','/' + screenName + '/groups'); + } + else if(stream == 'statuses/followers.json') { + history.pushState({strm:stream},'','/' + window.loginUsername + '/subscribers'); + } + else if(stream == 'statuses/friends.json') { + history.pushState({strm:stream},'','/' + window.loginUsername + '/subscriptions'); + } + else if(stream == 'statuses/mentions.json') { + history.pushState({strm:stream},'','/' + window.loginUsername + '/replies'); + } + else if(stream == 'favorites.json') { + history.pushState({strm:stream},'','/' + window.loginUsername + '/favorites'); + } + else if(stream == 'statusnet/groups/list.json') { + history.pushState({strm:stream},'','/' + window.loginUsername + '/groups'); + } + else if (stream.substring(0,27) == 'statuses/user_timeline.json') { + var screenName = stream.substring(stream.indexOf('=')+1); + history.pushState({strm:stream},'','/' + screenName); + } + else if(stream == 'statuses/friends_timeline.json') { + history.pushState({strm:stream},'','/' + window.loginUsername + '/all'); + } + else if(stream.substring(0,43) == 'statuses/friends_timeline.json?screen_name=') { + var screenName = stream.substring(stream.indexOf('=')+1); + history.pushState({strm:stream},'','/' + screenName + '/all'); + } + else if (stream == 'statuses/mentions.json') { + history.pushState({strm:stream},'','/' + window.loginUsername + '/replies'); + } + else if(stream == 'statuses/public_timeline.json') { + history.pushState({strm:stream},'','/'); + } + else if(stream.substring(0,26) == 'statusnet/groups/timeline/') { + var groupName = stream.substring(stream.indexOf('/timeline/')+10,stream.indexOf('.json')); + history.pushState({strm:stream},'','/group/' + groupName); + } + else if(stream.substring(0,24) == 'statusnet/tags/timeline/') { + var tagName = stream.substring(stream.indexOf('/timeline/')+10,stream.indexOf('.json')); + history.pushState({strm:stream},'','/tag/' + tagName); + } + else if(stream.substring(0,11) == 'search.json') { + var searchTerms = stream.substring(stream.indexOf('?q=')+3); + history.pushState({strm:stream},'','/search/notice?q=' + searchTerms); + } + } + + + +/* · + · + · Get stream from location bar + · + · @param stream: the stream, e.g. 'public_timeline.json' + · + · · · · · · · · · */ + +function getStreamFromUrl() { + var loc = window.location.href.replace('http://','').replace('https://','').replace(window.siteRootDomain,''); + + // default/fallback + var streamToSet = 'statuses/public_timeline.json'; + + // {domain}/{screen_name} + if ((/^[a-zA-Z0-9]+$/.test(loc.replace('/','')))) { + var userToStream = loc.replace('/',''); + if(userToStream.length>0) { + streamToSet = 'statuses/user_timeline.json?screen_name=' + userToStream; + } + } + + // {domain}/{screen_name}/all + else if ((/^[a-zA-Z0-9]+$/.test(loc.replace('/','').replace('/all','')))) { + var userToStream = loc.replace('/','').replace('/all',''); + if(userToStream.length>0) { + if(window.loginUsername == userToStream) { + streamToSet = 'statuses/friends_timeline.json'; + } + else { + streamToSet = 'statuses/friends_timeline.json?screen_name=' + userToStream; + } + } + } + + // {domain}/{screen_name}/replies + else if ((/^[a-zA-Z0-9]+$/.test(loc.replace('/','').replace('/replies','')))) { + var userToStream = loc.replace('/','').replace('/replies',''); + if(userToStream.length>0) { + if(window.loginUsername == userToStream) { + streamToSet = 'statuses/mentions.json'; + } + else { + streamToSet = 'statuses/mentions.json?screen_name=' + userToStream; + } + } + } + + // {domain}/{screen_name}/favorites + else if ((/^[a-zA-Z0-9]+$/.test(loc.replace('/','').replace('/favorites','')))) { + var userToStream = loc.replace('/','').replace('/favorites',''); + if(userToStream.length>0) { + if(window.loginUsername == userToStream) { + streamToSet = 'favorites.json'; + } + else { + streamToSet = 'favorites.json?screen_name=' + userToStream; + } + } + } + + // {domain}/{screen_name}/subscribers + else if ((/^[a-zA-Z0-9]+$/.test(loc.replace('/','').replace('/subscribers','')))) { + var userToStream = loc.replace('/','').replace('/subscribers',''); + if(userToStream.length>0) { + if(window.loginUsername == userToStream) { + streamToSet = 'statuses/followers.json'; + } + else { + streamToSet = 'statuses/followers.json?screen_name=' + userToStream; + } + } + } + + // {domain}/{screen_name}/subscriptions + else if ((/^[a-zA-Z0-9]+$/.test(loc.replace('/','').replace('/subscriptions','')))) { + var userToStream = loc.replace('/','').replace('/subscriptions',''); + if(userToStream.length>0) { + if(window.loginUsername == userToStream) { + streamToSet = 'statuses/friends.json'; + } + else { + streamToSet = 'statuses/friends.json?screen_name=' + userToStream; + } + } + } + + // {domain}/{screen_name}/groups + else if ((/^[a-zA-Z0-9]+$/.test(loc.replace('/','').replace('/groups','')))) { + var userToStream = loc.replace('/','').replace('/groups',''); + if(userToStream.length>0) { + if(window.loginUsername == userToStream) { + streamToSet = 'statusnet/groups/list.json'; + } + else { + streamToSet = 'statusnet/groups/list.json?screen_name=' + userToStream; + } + } + } + + // {domain}/tag/{tag} + else if (loc.indexOf('/tag/')>-1) { + var tagToStream = loc.replace('/tag/',''); + if(tagToStream.length>0) { + streamToSet = 'statusnet/tags/timeline/' + tagToStream + '.json'; + } + } + + // {domain}/group/{group} + else if (loc.indexOf('/group/')>-1) { // we assume we don't get any /group/{id}/id-urls, because we shouldn't + var groupToStream = loc.replace('/group/',''); + if(groupToStream.length>0) { + streamToSet = 'statusnet/groups/timeline/' + groupToStream + '.json'; + } + } + + // {domain}/search/notice?q={urlencoded searh terms} + else if (loc.indexOf('/search/notice?q=')>-1) { + var searchToStream = loc.replace('/search/notice?q=',''); + if(userToStream.length>0) { + streamToSet = 'search.json?q=' + searchToStream; + } + } + return streamToSet; + } + + +/* · + · + · Expand/de-expand queet + · + · @param q: the stream item to expand + · + · · · · · · · · · */ + +function expand_queet(q) { + + var qid = q.attr('data-quitter-id'); + + // de-expand if expanded + if(q.hasClass('expanded') && !q.hasClass('collapsing')) { + q.addClass('collapsing'); + q.find('.stream-item-expand').html(window.sL.expand); + if(q.hasClass('conversation')) { + q.removeClass('expanded'); + q.removeClass('collapsing'); + q.find('.expanded-content').remove(); + q.find('.view-more-container-top').remove(); + q.find('.view-more-container-bottom').remove(); + q.find('.stream-item.conversation').remove(); + q.find('.inline-reply-queetbox').remove(); + } + else { + var collapseTime = 200 + q.find('.stream-item.conversation:not(.hidden-conversation)').length*50; + q.find('.expanded-content').slideUp(collapseTime,function(){$(this).remove();}); + q.find('.view-more-container-top').slideUp(collapseTime,function(){$(this).remove();}); + q.find('.view-more-container-bottom').slideUp(collapseTime,function(){$(this).remove();}); + q.find('.stream-item.conversation').slideUp(collapseTime,function(){$(this).remove();}); + q.find('.inline-reply-queetbox').slideUp(collapseTime,function(){$(this).remove();}); + backToMyScrollPos(q,qid,'animate'); + setTimeout(function(){ + q.removeClass('expanded'); + q.removeClass('collapsing'); + },collapseTime+500); + } + } + else { + + rememberMyScrollPos(q,qid,-8); + + q.addClass('expanded'); + + // not for acitivity + if(!q.hasClass('activity')) { + q.find('.stream-item-expand').html(window.sL.collapse); + + // if shortened queet, get full text (not if external) + if(!q.hasClass('external-conversation')) { + if(q.children('.queet').find('span.attachment.more').length>0) { + var attachmentId = q.children('.queet').find('span.attachment.more').attr('data-attachment-id'); + getFromAPI("attachment/" + attachmentId + ".json",function(data){ + if(data) { + q.children('.queet').find('.queet-text').html($.trim(data.replace(/@/gi,'').replace(/!/gi,'').replace(/#/gi,''))); + } + }); + } + } + + // add expanded container + var longdate = parseTwitterLongDate(q.find('.created-at').attr('data-created-at')); + var qurl = q.find('.created-at').find('a').attr('href'); + + var metadata = '' + longdate + ' · ' + window.sL.details + ''; + + // show expanded content + q.find('.stream-item-footer').after('
    '); + + // maybe show images + $.each(q.children('.queet').find('.queet-text').find('a'), function() { + var attachment_title = $(this).attr('title'); + if(typeof attachment_title != 'undefined') { + if(attachment_title.substr(attachment_title.length - 5) == '.jpeg' || + attachment_title.substr(attachment_title.length - 4) == '.png' || + attachment_title.substr(attachment_title.length - 4) == '.gif' || + attachment_title.substr(attachment_title.length - 4) == '.jpg') { + q.children('.queet').find('.expanded-content').prepend('
    '); + } + } + }); + + // get favs and rq:s (not if external) + if(!q.hasClass('external-conversation')) { + + // get and show favs + getFavsOrRequeetsForQueet('favs',qid,function(favs){ + if(favs) { + if(q.hasClass('expanded') && !q.hasClass('collapsing')) { + showFavsOrRequeetsInQueet(q,favs,'favs'); + } + } + }); + + // get and show requeets + getFavsOrRequeetsForQueet('retweets',qid,function(requeets){ + if(requeets) { + if(q.hasClass('expanded') && !q.hasClass('collapsing')) { + showFavsOrRequeetsInQueet(q,requeets,'requeets'); + } + } + }); + } + + // show conversation and reply form (but not if already in conversation) + if(!q.hasClass('conversation')) { + + // show conversation + showConversation(qid); + + // show inline reply form + q.find('#q-' + qid).append(replyFormHtml(q,qid)); + + } + } + } + } + + + + +/* · + · + · Get a reply form + · + · @param q: the stream item to open reply form in + · @param qid: queet id + · @return the html for the reply form + · + · · · · · · · · · */ + +function replyFormHtml(q,qid) { + // get all @:s + var user_screen_name = q.find('.queet').find('.screen-name').html().substring(1); + var user_screen_name_html = '@' + user_screen_name + ''; + var reply_to_screen_name = ''; + var reply_to_screen_name_html = ''; + if(q.attr('data-in-reply-to-screen-name').length>0 // not if not a reply + && q.attr('data-in-reply-to-screen-name') != $('#user-screen-name').html() // not if it's me + && q.attr('data-in-reply-to-screen-name') != user_screen_name // not same screen name twice + ) { + reply_to_screen_name = q.attr('data-in-reply-to-screen-name'); + reply_to_screen_name_html = ' @' + reply_to_screen_name + ''; + } + var more_reply_tos = ''; + $.each(q.find('.queet').find('.queet-text').find('.mention'),function(key,obj){ + if($(obj).html() != user_screen_name && $(obj).html() != reply_to_screen_name && $(obj).html() != $('#user-screen-name').html()) { + more_reply_tos = more_reply_tos + ' @' + $(obj).html() + ''; + } + }); + + return '
    ') + ' data-start-html="' + escape(user_screen_name_html + reply_to_screen_name_html + more_reply_tos) + '">' + window.sL.replyTo + ' ' + user_screen_name_html + reply_to_screen_name_html + more_reply_tos + ' 
    '; + } + + +/* · + · + · Popup for replies, deletes, etc + · + · @param popupId: popups id + · @param heading: popops header + · @param bodyHtml: popups body html + · @param footerHtml: popups footer html + · + · · · · · · · · · · · · · */ + +function popUpAction(popupId, heading, bodyHtml, footerHtml){ + var allFooterHtml = ''; + if(footerHtml) { + allFooterHtml = ''; + } + $('body').prepend(''); + var this_modal_height = $('#' + popupId).children('.modal-draggable').height(); + var this_modal_width = $('#' + popupId).children('.modal-draggable').width(); + var popupPos = $('#' + popupId).children('.modal-draggable').offset().top - $(window).scrollTop(); + if((popupPos-(this_modal_height/2))<5) { + var marginTop = 5-popupPos; + } + else { + var marginTop = 0-this_modal_height/2; + } + $('#' + popupId).children('.modal-draggable').css('margin-top', marginTop + 'px'); + $('#' + popupId).children('.modal-draggable').css('margin-left', '-' + (this_modal_width/2) + 'px'); + $('#' + popupId).children('.modal-draggable').draggable({ handle: ".modal-header" }); + $('#' + popupId).children('.modal-header').disableSelection(); + } + + + +/* · + · + · Expand inline reply form + · + · @param box: jQuery object for the queet box that we want to expand + · + · · · · · · · · · · · · · */ + +function expandInlineQueetBox(box) { + if(!box.hasClass('active')) { + box.addClass('active'); + // remove the "reply/svara/etc" before the mentions + var new_mentions_html = ''; + $.each(box.find('a'),function(key,obj){ + new_mentions_html = new_mentions_html + '' + $(obj).html() + ' '; + }); + box.html(new_mentions_html); + placeCaretAtEnd(document.getElementById(box.attr('id'))); + + // show toolbar/button + box.after('
    '); + + // count chars + countCharsInQueetBox(box,box.parent().find('.queet-toolbar .queet-counter'),box.parent().find('.queet-toolbar button')); + } + } + + + +/* · + · + · Show conversation + · + · This function has grown into a monster, needs fixing + · + · · · · · · · · · · · · · */ + +function showConversation(qid) { + + display_spinner(); + + // get conversation + getFromAPI('statusnet/conversation/' + $('#stream-item-' + qid).attr('data-conversation-id') + '.json?count=100', function(data){ if(data) { + + if(data && !$('#stream-item-' + qid).hasClass('collapsing')){ + // we have local conversation + if(data.length>1) { + var before_or_after = 'after'; + $.each(data, function (key,obj) { + + // switch to append after original queet + if(obj.id == qid) { + before_or_after = 'before'; + } + + // don't add clicked queet to DOM, but all else + // note: first we add the full conversation, but hidden + if(obj.id != qid) { + var queetTime = parseTwitterDate(obj.created_at); + + // we don't want to print 'null', someone might have that username! + var in_reply_to_screen_name = ''; + if(obj.in_reply_to_screen_name != null) { + in_reply_to_screen_name = obj.in_reply_to_screen_name; + } + + // requeet or delete html + var requeetedClass = ''; + if(obj.user.id == window.myUserID) { + var requeetHtml = '
  • ' + window.sL.deleteVerb + '
  • '; + } + else if(obj.repeated) { + var requeetHtml = '
  • ' + window.sL.requeetedVerb + '
  • '; + requeetedClass = 'requeeted'; + } + else { + var requeetHtml = '
  • ' + window.sL.requeetVerb + '
  • '; + } + // favorite html + var favoritedClass = ''; + if(obj.favorited) { + var favoriteHtml = ' ' + window.sL.favoritedVerb + ''; + favoritedClass = 'favorited'; + } + else { + var favoriteHtml = ' ' + window.sL.favoriteVerb + ''; + } + + if(obj.source == 'activity') { + var queetHtml = '
    ' + $.trim(obj.statusnet_html) + '
    '; + } + else { + var queetHtml = '
    ' + $.trim(obj.statusnet_html) + '
    '; + } + + // detect rtl + queetHtml = detectRTL(queetHtml); + + if($('#stream-item-' + qid).hasClass('expanded')) { // add queet to conversation only if still expanded + if(before_or_after == 'before') { + $('#stream-item-' + qid).prepend(queetHtml); + } + else { + $('#q-' + qid).after(queetHtml); + } + + } + } + remove_spinner(); + convertAttachmentMoreHref(); + }); + } + + // try to get conversation from external instance + else if(typeof data[0] != 'undefined') { + if(data[0].source == 'ostatus') { + var external_status_url = data[0].uri.replace('notice','api/statuses/show') + '.json'; + var external_base_url = data[0].uri.substring(0,data[0].uri.indexOf('/notice/')); + $.ajax({ url: external_status_url, type: "GET", dataType: "jsonp", success: function(data) { // try to get conversation id fro notice + var external_id = data.id; + if(typeof data.statusnet_conversation_id == 'undefined') { + console.log(data);remove_spinner(); + } + else { // proceed if we got a conversation_id + $.ajax({ url: external_base_url + '/api/statusnet/conversation/' + data.statusnet_conversation_id + ".json?count=100", type: "GET", dataType: "jsonp", success: function(data) { + var before_or_after = 'after'; + $.each(data, function (key,obj) { + + // switch to append after original queet + if(obj.id == external_id) { + before_or_after = 'before'; + $('#stream-item-' + qid).attr('data-in-reply-to-status-id',obj.in_reply_to_status_id); + } + else { + var queetTime = parseTwitterDate(obj.created_at); + + // we don't want to print 'null', someone might have that username! + var in_reply_to_screen_name = ''; + if(obj.in_reply_to_screen_name != null) { + in_reply_to_screen_name = obj.in_reply_to_screen_name; + } + + + if(obj.source == 'activity') { + var queetHtml = '
    ' + $.trim(obj.statusnet_html) + '
    '; + } + else { + var queetHtml = '
    ' + $.trim(obj.statusnet_html) + '
    '; + } + + // detect rtl + queetHtml = detectRTL(queetHtml); + + if($('#stream-item-' + qid).hasClass('expanded')) { // add queet to conversation only if still expanded + if(before_or_after == 'before') { + $('#stream-item-' + qid).prepend(queetHtml); + } + else { + $('#q-' + qid).after(queetHtml); + } + } + } + remove_spinner(); + convertAttachmentMoreHref(); + }); + findInReplyToStatusAndShow(qid,$('#stream-item-' + qid).attr('data-in-reply-to-status-id'),true,false); + backToMyScrollPos($('#q-' + qid),qid,false); + }, error: function(data) { console.log(data);remove_spinner(); }}); + } + }, error: function(data) { console.log(data);remove_spinner(); }}); + } + // no conversation + else { + remove_spinner(); + } + } + else { + remove_spinner(); + } + + // loop trough this stream items conversation and show the "strict" line of replies + findInReplyToStatusAndShow(qid,$('#stream-item-' + qid).attr('data-in-reply-to-status-id'),true,false); + backToMyScrollPos($('#q-' + qid),qid,false); + } + else { + remove_spinner(); + } + }}); + } + + + +/* · + · + · Recursive walker functions to view onlt reyplies to replies, not full conversation + · + · · · · · · · · · · · · · */ + +function findInReplyToStatusAndShow(qid,reply,only_first,onlyINreplyto) { + var reply_found = $('#stream-item-' + qid).find('.stream-item[data-quitter-id="' + reply + '"]'); + var reply_found_reply_to = $('#stream-item-' + qid).find('.stream-item[data-quitter-id="' + reply_found.attr('data-in-reply-to-status-id') + '"]'); + if(reply_found.length>0) { + reply_found.removeClass('hidden-conversation'); + reply_found.animate({opacity:'1'},500); + if(only_first && reply_found_reply_to.length>0) { + reply_found.before(''); + findReplyToStatusAndShow(qid,qid,true); + } + else { + findInReplyToStatusAndShow(qid,reply_found.attr('data-in-reply-to-status-id'),false,onlyINreplyto); + } + } + else if(!onlyINreplyto) { + findReplyToStatusAndShow(qid,qid,true); + } + else { + checkForHiddenConversationQueets(qid); + } + } +// recursive function to find the replies to a status +function findReplyToStatusAndShow(qid,this_id,only_first) { + var reply_found = $('#stream-item-' + qid).find('.stream-item[data-in-reply-to-status-id="' + this_id + '"]'); + var reply_founds_reply = $('#stream-item-' + qid).find('.stream-item[data-in-reply-to-status-id="' + reply_found.attr('data-quitter-id') + '"]'); + if(reply_found.length>0) { + reply_found.removeClass('hidden-conversation'); + reply_found.animate({opacity:'1'},100,function(){ + if(!only_first) { + findReplyToStatusAndShow(qid,$(this).attr('data-quitter-id'),false); + } + }); + if(only_first && reply_founds_reply.length>0) { + reply_found.last().after(''); + } + } + checkForHiddenConversationQueets(qid); + } + +// helper function for the above recursive functions +function checkForHiddenConversationQueets(qid) { + // here we check if there are any remaining hidden queets in conversation, if there are, we put a "show full conversation"-link + if($('#stream-item-' + qid).find('.hidden-conversation').length>0) { + if($('#stream-item-' + qid).children('.queet').find('.show-full-conversation').length == 0) { + $('#stream-item-' + qid).children('.queet').find('.metadata').append('' + window.sL.expandFullConversation + ''); + } + } + else { + $('#stream-item-' + qid).children('.queet').find('.show-full-conversation').remove(); + } + } + + +/* · + · + · Build stream items and add them to feed + · + · Also a function that has grown out of control... Needs total makeover + · + · · · · · · · · · · · · · */ + +function addToFeed(feed, after, extraClasses) { + + $.each(feed.reverse(), function (key,obj) { + + var extraClassesThisRun = extraClasses; + + // if this is a user feed + if(window.currentStream.substring(0,21) == 'statuses/friends.json' || window.currentStream.substring(0,18) == 'statuses/followers') { + obj.description = obj.description || ''; + + // show user actions + var followingClass = ''; + if(obj.following) { + followingClass = 'following'; + } + var followButton = ''; + if(typeof window.loginUsername != 'undefined' // if logged in + && window.myUserID != obj.id) { // not if this is me + if(!(obj.statusnet_profile_url.indexOf('/twitter.com/')>-1 && obj.following === false)) { // only unfollow twitter users + var followButton = ''; + } + } + + var userHtml = '
    ' + followButton + '
    '; + $('#feed-body').prepend(userHtml); + } + + // if this is a group feed + else if(window.currentStream.substring(0,26) == 'statusnet/groups/list.json') { + + obj.description = obj.description || ''; + obj.stream_logo = obj.stream_logo || 'http://quitter.se/theme/quitter-theme2/default-avatar-stream.png'; + + // show group actions if logged in + var memberClass = ''; + if(obj.member) { + memberClass = 'member'; + } + var memberButton = ''; + if(typeof window.loginUsername != 'undefined') { + var memberButton = ''; + } + + var groupHtml = '
    ' + memberButton + '
    '; + $('#feed-body').prepend(groupHtml); + } + + // if this is a retweet + else if(typeof obj.retweeted_status != 'undefined') { + + // retweeted object already exist in feed + if($('#q-' + obj.retweeted_status.id).length > 0) { + + // only if not already shown and not mine + if($('#requeet-' + obj.id).length == 0 && obj.user.statusnet_profile_url != $('#user-profile-link').children('a').attr('href')) { + + // if not requeeted before + if($('#q-' + obj.retweeted_status.id + ' .stream-item-footer').find('.requeet-text').length > 0) { + // if users rt not already added + if($('#q-' + obj.retweeted_status.id + ' .stream-item-footer').find('.requeet-text').find('a[data-user-id="' + obj.user.id + '"]').length==0) { + $('#q-' + obj.retweeted_status.id + ' .stream-item-footer').find('.requeet-text').append(', ' + obj.user.name + ''); + } + } + else { + $('#q-' + obj.retweeted_status.id + ' .stream-item-footer').prepend('
    ' + window.sL.requeetedBy + ' ' + obj.user.name + '
    '); + } + } + } + // retweeted object don't exist in feed and not mine + else if(obj.user.statusnet_profile_url != $('#user-profile-link').children('a').attr('href')){ + + // we don't want to print 'null', someone might have that username! + var in_reply_to_screen_name = ''; + if(obj.retweeted_status.in_reply_to_screen_name != null) { + in_reply_to_screen_name = obj.retweeted_status.in_reply_to_screen_name; + } + + // requeet html + var requeetedClass = ''; + if(obj.retweeted_status.user.id == window.myUserID) { + var requeetHtml = '
  • ' + window.sL.deleteVerb + '
  • '; + } + else if(obj.retweeted_status.repeated) { + var requeetHtml = '
  • ' + window.sL.requeetedVerb + ''; + requeetedClass = 'requeeted'; + } + else { + var requeetHtml = '
  • ' + window.sL.requeetVerb + ''; + } + // favorite html + var favoritedClass = ''; + if(obj.retweeted_status.favorited) { + var favoriteHtml = ' ' + window.sL.favoritedVerb + ''; + favoritedClass = 'favorited'; + } + else { + var favoriteHtml = ' ' + window.sL.favoriteVerb + ''; + } + + // actions only for logged in users + var queetActions = ''; + var expandHTML = ''; + if(typeof window.loginUsername != 'undefined') { + queetActions = ''; + expandHTML = '' + window.sL.expand + ''; + } + + var queetTime = parseTwitterDate(obj.retweeted_status.created_at); + var queetHtml = '
    ' + $.trim(obj.retweeted_status.statusnet_html) + '
    '; + + // detect rtl + queetHtml = detectRTL(queetHtml); + + if(after) { + $('#' + after).after(queetHtml); + } + else { + $('#feed-body').prepend(queetHtml); + } + + } + + } + + // ordinary tweet + else { + + // only if not already exist + if($('#q-' + obj.id).length == 0) { + + // activity get special design + if(obj.source == 'activity') { + var queetTime = parseTwitterDate(obj.created_at); + var queetHtml = '
    ' + $.trim(obj.statusnet_html) + '
    '; + + // detect rtl + queetHtml = detectRTL(queetHtml); + + if(after) { + $('#' + after).after(queetHtml); + } + else { + $('#feed-body').prepend(queetHtml); + } + + } + else { + + // if this is my queet, remove any temp-queets + if(typeof obj.user != 'undefined') { + if(obj.user.screen_name == $('#user-screen-name').html()) { + if($('.temp-post').length > 0) { + $('.temp-post').each(function (){ + // remove temp duplicate + $(this).css('display','none'); + + // we do this so this queet gets added after correct temp-queet in expanded conversations + if($(this).find('.queet-text').text() == obj.text) { + after = $(this).attr('id'); + } + + // but don't hide my new queet + extraClassesThisRun = 'visible'; + }); + } + } + } + + // we don't want to print 'null' in in_reply_to_screen_name-attribute, someone might have that username! + var in_reply_to_screen_name = ''; + if(obj.in_reply_to_screen_name != null) { + in_reply_to_screen_name = obj.in_reply_to_screen_name; + } + + // requeet html + var requeetedClass = ''; + if(obj.user.id == window.myUserID) { + var requeetHtml = '
  • ' + window.sL.deleteVerb + '
  • '; + } + else if(obj.repeated) { + var requeetHtml = '
  • ' + window.sL.requeetedVerb + '
  • '; + var requeetedClass = 'requeeted'; + } + else { + var requeetHtml = '
  • ' + window.sL.requeetVerb + '
  • '; + } + // favorite html + var favoritedClass = ''; + if(obj.favorited) { + var favoriteHtml = ' ' + window.sL.favoritedVerb + ''; + favoritedClass = 'favorited'; + } + else { + var favoriteHtml = ' ' + window.sL.favoriteVerb + ''; + } + + + // actions only for logged in users + var queetActions = ''; + var expandHTML = ''; + if(typeof window.loginUsername != 'undefined') { + queetActions = ''; + expandHTML = '' + window.sL.expand + ''; + } + + var queetTime = parseTwitterDate(obj.created_at); + var queetHtml = '
    ' + $.trim(obj.statusnet_html) + '
    '; + + // detect rtl + queetHtml = detectRTL(queetHtml); + + if(after) { + if($('#' + after).hasClass('conversation')) { // if this is a reply, give stream item some conversation formatting + if($('#conversation-q-' + obj.id).length == 0) { // only if it's not already there + $('#' + after).after(queetHtml.replace('id="stream-item','id="conversation-stream-item').replace('class="stream-item','class="stream-item conversation').replace('id="q','id="conversation-q')); + $('#' + after).remove(); + } + } + else { + $('#' + after).after(queetHtml); + } + } + else { + $('#feed-body').prepend(queetHtml); + } + + } + } + } + + convertAttachmentMoreHref(); + }); + $('.stream-selection').removeAttr('data-current-user-stream-name'); // don't remeber user feeds + } + + +/* · + · + · View threaded converation + · + · @param id: the stream item id + · + · · · · · · · · · · · · · */ + +$('body').on('click','.longdate',function(){ + threadedConversation($(this).closest('.stream-item:not(.conversation)').attr('data-quitter-id')); + }) +function threadedConversation(id){ + $('body').prepend(''); + var scrollTop = $(window).scrollTop(); + var containerStreamItem = $('#stream-item-' + id); + if(containerStreamItem.children('div:first-child').hasClass('.queet')) { + var firstStreamItemId = id; + } + else { + var firstStreamItemId = containerStreamItem.children('div:first-child').attr('data-quitter-id'); + } + getThreadedReply(id,firstStreamItemId,$('#threaded-' + id + ' .thread-container div')); + } + +function getThreadedReply(containerStreamId,this_id,appendToObj) { + + var $this_item = $('
    ').append($('.stream-item[data-quitter-id="' + this_id + '"]').outerHTML()); + $this_item.children().children().remove('.stream-item.conversation'); + $this_item.children('.stream-item').css('margin-left',parseInt(appendToObj.css('margin-left'),10)+20 + 'px'); + $this_item.children('.stream-item').removeClass('hidden-conversation'); + $this_item.children('.stream-item').removeClass('expanded'); + $this_item.children('.stream-item').removeClass('activity'); + $this_item.children('.stream-item').removeClass('conversation'); + $this_item.children('.stream-item').removeClass('visible'); + $this_item.children('.stream-item').children('div:not(.queet)').remove(); + $this_item.children('.stream-item').find('.inline-reply-queetbox').remove(); + $this_item.children('.stream-item').find('.expanded-content').remove(); + $this_item.children('.stream-item').find('.stream-item-expand').remove(); + $this_item.children('.stream-item').css('opacity','1'); + appendToObj.after($this_item.html()); + + $.each($('.stream-item[data-quitter-id="' + containerStreamId + '"]').children().get().reverse(),function(){ + if($(this).hasClass('queet')) { + var this_reply_to = $(this).parent().attr('data-in-reply-to-status-id'); + var childs_id = $(this).parent().attr('data-quitter-id'); + } + else { + var this_reply_to = $(this).attr('data-in-reply-to-status-id'); + var childs_id = $(this).attr('data-quitter-id'); + } + if(this_id == this_reply_to) { + getThreadedReply(containerStreamId,childs_id,$('#threaded-' + containerStreamId + ' .stream-item[data-quitter-id="' + this_id + '"]')); + } + }); + } diff --git a/js/jquery-2.0.2.min.js b/js/jquery-2.0.2.min.js new file mode 100644 index 0000000..73e5218 --- /dev/null +++ b/js/jquery-2.0.2.min.js @@ -0,0 +1,6 @@ +/*! jQuery v2.0.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license +//@ sourceMappingURL=jquery-2.0.2.min.map +*/ +(function(e,undefined){var t,n,r=typeof undefined,i=e.location,o=e.document,s=o.documentElement,a=e.jQuery,u=e.$,l={},c=[],p="2.0.2",f=c.concat,h=c.push,d=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=p.trim,x=function(e,n){return new x.fn.init(e,n,t)},b=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^-ms-/,N=/-([\da-z])/gi,E=function(e,t){return t.toUpperCase()},S=function(){o.removeEventListener("DOMContentLoaded",S,!1),e.removeEventListener("load",S,!1),x.ready()};x.fn=x.prototype={jquery:p,constructor:x,init:function(e,t,n){var r,i;if(!e)return this;if("string"==typeof e){if(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:T.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:o,!0)),C.test(r[1])&&x.isPlainObject(t))for(r in t)x.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=o.getElementById(r[2]),i&&i.parentNode&&(this.length=1,this[0]=i),this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?n.ready(e):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return d.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,t,n,r,i,o,s=arguments[0]||{},a=1,u=arguments.length,l=!1;for("boolean"==typeof s&&(l=s,s=arguments[1]||{},a=2),"object"==typeof s||x.isFunction(s)||(s={}),u===a&&(s=this,--a);u>a;a++)if(null!=(e=arguments[a]))for(t in e)n=s[t],r=e[t],s!==r&&(l&&r&&(x.isPlainObject(r)||(i=x.isArray(r)))?(i?(i=!1,o=n&&x.isArray(n)?n:[]):o=n&&x.isPlainObject(n)?n:{},s[t]=x.extend(l,o,r)):r!==undefined&&(s[t]=r));return s},x.extend({expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=a),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){(e===!0?--x.readyWait:x.isReady)||(x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(o,[x]),x.fn.trigger&&x(o).trigger("ready").off("ready")))},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray,isWindow:function(e){return null!=e&&e===e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if("object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(t){return!1}return!0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:JSON.parse,parseXML:function(e){var t,n;if(!e||"string"!=typeof e)return null;try{n=new DOMParser,t=n.parseFromString(e,"text/xml")}catch(r){t=undefined}return(!t||t.getElementsByTagName("parsererror").length)&&x.error("Invalid XML: "+e),t},noop:function(){},globalEval:function(e){var t,n=eval;e=x.trim(e),e&&(1===e.indexOf("use strict")?(t=o.createElement("script"),t.text=e,o.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(k,"ms-").replace(N,E)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,s=j(e);if(n){if(s){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(s){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:function(e){return null==e?"":v.call(e)},makeArray:function(e,t){var n=t||[];return null!=e&&(j(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:g.call(t,e,n)},merge:function(e,t){var n=t.length,r=e.length,i=0;if("number"==typeof n)for(;n>i;i++)e[r++]=t[i];else while(t[i]!==undefined)e[r++]=t[i++];return e.length=r,e},grep:function(e,t,n){var r,i=[],o=0,s=e.length;for(n=!!n;s>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,s=j(e),a=[];if(s)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(a[a.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(a[a.length]=r);return f.apply([],a)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(n=e[t],t=e,e=n),x.isFunction(e)?(r=d.call(arguments,2),i=function(){return e.apply(t||this,r.concat(d.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):undefined},access:function(e,t,n,r,i,o,s){var a=0,u=e.length,l=null==n;if("object"===x.type(n)){i=!0;for(a in n)x.access(e,t,a,n[a],!0,o,s)}else if(r!==undefined&&(i=!0,x.isFunction(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(x(e),n)})),t))for(;u>a;a++)t(e[a],n,s?r:r.call(e[a],a,t(e[a],n)));return i?e:l?t.call(e):u?t(e[0],n):o},now:Date.now,swap:function(e,t,n,r){var i,o,s={};for(o in t)s[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=s[o];return i}}),x.ready.promise=function(t){return n||(n=x.Deferred(),"complete"===o.readyState?setTimeout(x.ready):(o.addEventListener("DOMContentLoaded",S,!1),e.addEventListener("load",S,!1))),n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function j(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}t=x(o),function(e,undefined){var t,n,r,i,o,s,a,u,l,c,p,f,h,d,g,m,y,v="sizzle"+-new Date,b=e.document,w=0,T=0,C=at(),k=at(),N=at(),E=!1,S=function(){return 0},j=typeof undefined,D=1<<31,A={}.hasOwnProperty,L=[],H=L.pop,q=L.push,O=L.push,F=L.slice,P=L.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",W="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",$=W.replace("w","w#"),B="\\["+M+"*("+W+")"+M+"*(?:([*^$|!~]?=)"+M+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+$+")|)|)"+M+"*\\]",I=":("+W+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+B.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=RegExp("^"+M+"*,"+M+"*"),X=RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=RegExp(M+"*[+~]"),Y=RegExp("="+M+"*([^\\]'\"]*)"+M+"*\\]","g"),V=RegExp(I),G=RegExp("^"+$+"$"),J={ID:RegExp("^#("+W+")"),CLASS:RegExp("^\\.("+W+")"),TAG:RegExp("^("+W.replace("w","w*")+")"),ATTR:RegExp("^"+B),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:RegExp("^(?:"+R+")$","i"),needsContext:RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Q=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/^(?:input|select|textarea|button)$/i,et=/^h\d$/i,tt=/'|\\/g,nt=RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),rt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{O.apply(L=F.call(b.childNodes),b.childNodes),L[b.childNodes.length].nodeType}catch(it){O={apply:L.length?function(e,t){q.apply(e,F.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function ot(e,t,r,i){var o,s,a,u,l,f,g,m,x,w;if((t?t.ownerDocument||t:b)!==p&&c(t),t=t||p,r=r||[],!e||"string"!=typeof e)return r;if(1!==(u=t.nodeType)&&9!==u)return[];if(h&&!i){if(o=K.exec(e))if(a=o[1]){if(9===u){if(s=t.getElementById(a),!s||!s.parentNode)return r;if(s.id===a)return r.push(s),r}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(a))&&y(t,s)&&s.id===a)return r.push(s),r}else{if(o[2])return O.apply(r,t.getElementsByTagName(e)),r;if((a=o[3])&&n.getElementsByClassName&&t.getElementsByClassName)return O.apply(r,t.getElementsByClassName(a)),r}if(n.qsa&&(!d||!d.test(e))){if(m=g=v,x=t,w=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){f=vt(e),(g=t.getAttribute("id"))?m=g.replace(tt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",l=f.length;while(l--)f[l]=m+xt(f[l]);x=U.test(e)&&t.parentNode||t,w=f.join(",")}if(w)try{return O.apply(r,x.querySelectorAll(w)),r}catch(T){}finally{g||t.removeAttribute("id")}}}return St(e.replace(z,"$1"),t,r,i)}function st(e){return Q.test(e+"")}function at(){var e=[];function t(n,r){return e.push(n+=" ")>i.cacheLength&&delete t[e.shift()],t[n]=r}return t}function ut(e){return e[v]=!0,e}function lt(e){var t=p.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t,n){e=e.split("|");var r,o=e.length,s=n?null:t;while(o--)(r=i.attrHandle[e[o]])&&r!==t||(i.attrHandle[e[o]]=s)}function pt(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:e[t]===!0?t.toLowerCase():null}function ft(e,t){return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}function ht(e){return"input"===e.nodeName.toLowerCase()?e.defaultValue:undefined}function dt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function gt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function mt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function yt(e){return ut(function(t){return t=+t,ut(function(n,r){var i,o=e([],n.length,t),s=o.length;while(s--)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}s=ot.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},n=ot.support={},c=ot.setDocument=function(e){var t=e?e.ownerDocument||e:b,r=t.parentWindow;return t!==p&&9===t.nodeType&&t.documentElement?(p=t,f=t.documentElement,h=!s(t),r&&r.frameElement&&r.attachEvent("onbeforeunload",function(){c()}),n.attributes=lt(function(e){return e.innerHTML="",ct("type|href|height|width",ft,"#"===e.firstChild.getAttribute("href")),ct(R,pt,null==e.getAttribute("disabled")),e.className="i",!e.getAttribute("className")}),n.input=lt(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")}),ct("value",ht,n.attributes&&n.input),n.getElementsByTagName=lt(function(e){return e.appendChild(t.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=lt(function(e){return e.innerHTML="
    ",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),n.getById=lt(function(e){return f.appendChild(e).id=v,!t.getElementsByName||!t.getElementsByName(v).length}),n.getById?(i.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){return e.getAttribute("id")===t}}):(delete i.find.ID,i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=n.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==j?t.getElementsByTagName(e):undefined}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.CLASS=n.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==j&&h?t.getElementsByClassName(e):undefined},g=[],d=[],(n.qsa=st(t.querySelectorAll))&&(lt(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll(":checked").length||d.push(":checked")}),lt(function(e){var n=t.createElement("input");n.setAttribute("type","hidden"),e.appendChild(n).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&d.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||d.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),d.push(",.*:")})),(n.matchesSelector=st(m=f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&<(function(e){n.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",I)}),d=d.length&&RegExp(d.join("|")),g=g.length&&RegExp(g.join("|")),y=st(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},n.sortDetached=lt(function(e){return 1&e.compareDocumentPosition(t.createElement("div"))}),S=f.compareDocumentPosition?function(e,r){if(e===r)return E=!0,0;var i=r.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(r);return i?1&i||!n.sortDetached&&r.compareDocumentPosition(e)===i?e===t||y(b,e)?-1:r===t||y(b,r)?1:l?P.call(l,e)-P.call(l,r):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,n){var r,i=0,o=e.parentNode,s=n.parentNode,a=[e],u=[n];if(e===n)return E=!0,0;if(!o||!s)return e===t?-1:n===t?1:o?-1:s?1:l?P.call(l,e)-P.call(l,n):0;if(o===s)return dt(e,n);r=e;while(r=r.parentNode)a.unshift(r);r=n;while(r=r.parentNode)u.unshift(r);while(a[i]===u[i])i++;return i?dt(a[i],u[i]):a[i]===b?-1:u[i]===b?1:0},t):p},ot.matches=function(e,t){return ot(e,null,null,t)},ot.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Y,"='$1']"),!(!n.matchesSelector||!h||g&&g.test(t)||d&&d.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return ot(t,p,null,[e]).length>0},ot.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},ot.attr=function(e,t){(e.ownerDocument||e)!==p&&c(e);var r=i.attrHandle[t.toLowerCase()],o=r&&A.call(i.attrHandle,t.toLowerCase())?r(e,t,!h):undefined;return o===undefined?n.attributes||!h?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null:o},ot.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},ot.uniqueSort=function(e){var t,r=[],i=0,o=0;if(E=!n.detectDuplicates,l=!n.sortStable&&e.slice(0),e.sort(S),E){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return e},o=ot.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=ot.selectors={cacheLength:50,createPseudo:ut,match:J,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(nt,rt),e[3]=(e[4]||e[5]||"").replace(nt,rt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ot.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ot.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return J.CHILD.test(e[0])?null:(e[3]&&e[4]!==undefined?e[2]=e[4]:n&&V.test(n)&&(t=vt(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(nt,rt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=C[e+" "];return t||(t=RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&C(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ot.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,h,d,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,y=a&&t.nodeName.toLowerCase(),x=!u&&!a;if(m){if(o){while(g){p=t;while(p=p[g])if(a?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;d=g="only"===e&&!d&&"nextSibling"}return!0}if(d=[s?m.firstChild:m.lastChild],s&&x){c=m[v]||(m[v]={}),l=c[e]||[],h=l[0]===w&&l[1],f=l[0]===w&&l[2],p=h&&m.childNodes[h];while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[w,h,f];break}}else if(x&&(l=(t[v]||(t[v]={}))[e])&&l[0]===w)f=l[1];else while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if((a?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(x&&((p[v]||(p[v]={}))[e]=[w,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||ot.error("unsupported pseudo: "+e);return r[v]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?ut(function(e,n){var i,o=r(e,t),s=o.length;while(s--)i=P.call(e,o[s]),e[i]=!(n[i]=o[s])}):function(e){return r(e,0,n)}):r}},pseudos:{not:ut(function(e){var t=[],n=[],r=a(e.replace(z,"$1"));return r[v]?ut(function(e,t,n,i){var o,s=r(e,null,i,[]),a=e.length;while(a--)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:ut(function(e){return function(t){return ot(e,t).length>0}}),contains:ut(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:ut(function(e){return G.test(e||"")||ot.error("unsupported lang: "+e),e=e.replace(nt,rt).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return et.test(e.nodeName)},input:function(e){return Z.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:yt(function(){return[0]}),last:yt(function(e,t){return[t-1]}),eq:yt(function(e,t,n){return[0>n?n+t:n]}),even:yt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:yt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:yt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:yt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[t]=gt(t);for(t in{submit:!0,reset:!0})i.pseudos[t]=mt(t);function vt(e,t){var n,r,o,s,a,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);a=e,u=[],l=i.preFilter;while(a){(!n||(r=_.exec(a)))&&(r&&(a=a.slice(r[0].length)||a),u.push(o=[])),n=!1,(r=X.exec(a))&&(n=r.shift(),o.push({value:n,type:r[0].replace(z," ")}),a=a.slice(n.length));for(s in i.filter)!(r=J[s].exec(a))||l[s]&&!(r=l[s](r))||(n=r.shift(),o.push({value:n,type:s,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?ot.error(e):k(e,u).slice(0)}function xt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function bt(e,t,n){var i=t.dir,o=n&&"parentNode"===i,s=T++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,a){var u,l,c,p=w+" "+s;if(a){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,a))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[v]||(t[v]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,a)||r,l[1]===!0)return!0}}function wt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function Tt(e,t,n,r,i){var o,s=[],a=0,u=e.length,l=null!=t;for(;u>a;a++)(o=e[a])&&(!n||n(o,r,i))&&(s.push(o),l&&t.push(a));return s}function Ct(e,t,n,r,i,o){return r&&!r[v]&&(r=Ct(r)),i&&!i[v]&&(i=Ct(i,o)),ut(function(o,s,a,u){var l,c,p,f=[],h=[],d=s.length,g=o||Et(t||"*",a.nodeType?[a]:a,[]),m=!e||!o&&t?g:Tt(g,f,e,a,u),y=n?i||(o?e:d||r)?[]:s:m;if(n&&n(m,y,a,u),r){l=Tt(y,h),r(l,[],a,u),c=l.length;while(c--)(p=l[c])&&(y[h[c]]=!(m[h[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?P.call(o,p):f[c])>-1&&(o[l]=!(s[l]=p))}}else y=Tt(y===s?y.splice(d,y.length):y),i?i(null,s,y,u):O.apply(s,y)})}function kt(e){var t,n,r,o=e.length,s=i.relative[e[0].type],a=s||i.relative[" "],l=s?1:0,c=bt(function(e){return e===t},a,!0),p=bt(function(e){return P.call(t,e)>-1},a,!0),f=[function(e,n,r){return!s&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>l;l++)if(n=i.relative[e[l].type])f=[bt(wt(f),n)];else{if(n=i.filter[e[l].type].apply(null,e[l].matches),n[v]){for(r=++l;o>r;r++)if(i.relative[e[r].type])break;return Ct(l>1&&wt(f),l>1&&xt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&kt(e.slice(l,r)),o>r&&kt(e=e.slice(r)),o>r&&xt(e))}f.push(n)}return wt(f)}function Nt(e,t){var n=0,o=t.length>0,s=e.length>0,a=function(a,l,c,f,h){var d,g,m,y=[],v=0,x="0",b=a&&[],T=null!=h,C=u,k=a||s&&i.find.TAG("*",h&&l.parentNode||l),N=w+=null==C?1:Math.random()||.1;for(T&&(u=l!==p&&l,r=n);null!=(d=k[x]);x++){if(s&&d){g=0;while(m=e[g++])if(m(d,l,c)){f.push(d);break}T&&(w=N,r=++n)}o&&((d=!m&&d)&&v--,a&&b.push(d))}if(v+=x,o&&x!==v){g=0;while(m=t[g++])m(b,y,l,c);if(a){if(v>0)while(x--)b[x]||y[x]||(y[x]=H.call(f));y=Tt(y)}O.apply(f,y),T&&!a&&y.length>0&&v+t.length>1&&ot.uniqueSort(f)}return T&&(w=N,u=C),b};return o?ut(a):a}a=ot.compile=function(e,t){var n,r=[],i=[],o=N[e+" "];if(!o){t||(t=vt(e)),n=t.length;while(n--)o=kt(t[n]),o[v]?r.push(o):i.push(o);o=N(e,Nt(i,r))}return o};function Et(e,t,n){var r=0,i=t.length;for(;i>r;r++)ot(e,t[r],n);return n}function St(e,t,r,o){var s,u,l,c,p,f=vt(e);if(!o&&1===f.length){if(u=f[0]=f[0].slice(0),u.length>2&&"ID"===(l=u[0]).type&&n.getById&&9===t.nodeType&&h&&i.relative[u[1].type]){if(t=(i.find.ID(l.matches[0].replace(nt,rt),t)||[])[0],!t)return r;e=e.slice(u.shift().value.length)}s=J.needsContext.test(e)?0:u.length;while(s--){if(l=u[s],i.relative[c=l.type])break;if((p=i.find[c])&&(o=p(l.matches[0].replace(nt,rt),U.test(u[0].type)&&t.parentNode||t))){if(u.splice(s,1),e=o.length&&xt(u),!e)return O.apply(r,o),r;break}}}return a(e,f)(o,t,!h,r,U.test(e)),r}i.pseudos.nth=i.pseudos.eq;function jt(){}jt.prototype=i.filters=i.pseudos,i.setFilters=new jt,n.sortStable=v.split("").sort(S).join("")===v,c(),[0,0].sort(S),n.detectDuplicates=E,x.find=ot,x.expr=ot.selectors,x.expr[":"]=x.expr.pseudos,x.unique=ot.uniqueSort,x.text=ot.getText,x.isXMLDoc=ot.isXML,x.contains=ot.contains}(e);var D={};function A(e){var t=D[e]={};return x.each(e.match(w)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?D[e]||A(e):x.extend({},e);var t,n,r,i,o,s,a=[],u=!e.once&&[],l=function(p){for(t=e.memory&&p,n=!0,s=i||0,i=0,o=a.length,r=!0;a&&o>s;s++)if(a[s].apply(p[0],p[1])===!1&&e.stopOnFalse){t=!1;break}r=!1,a&&(u?u.length&&l(u.shift()):t?a=[]:c.disable())},c={add:function(){if(a){var n=a.length;(function s(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&c.has(n)||a.push(n):n&&n.length&&"string"!==r&&s(n)})})(arguments),r?o=a.length:t&&(i=n,l(t))}return this},remove:function(){return a&&x.each(arguments,function(e,t){var n;while((n=x.inArray(t,a,n))>-1)a.splice(n,1),r&&(o>=n&&o--,s>=n&&s--)}),this},has:function(e){return e?x.inArray(e,a)>-1:!(!a||!a.length)},empty:function(){return a=[],o=0,this},disable:function(){return a=u=t=undefined,this},disabled:function(){return!a},lock:function(){return u=undefined,t||c.disable(),this},locked:function(){return!u},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!a||n&&!u||(r?u.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!n}};return c},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var s=o[0],a=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var s=o[2],a=o[3];r[o[1]]=s.add,a&&s.add(function(){n=a},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=s.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=d.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),s=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?d.call(arguments):r,n===a?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},a,u,l;if(r>1)for(a=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(s(t,l,n)).fail(o.reject).progress(s(t,u,a)):--i;return i||o.resolveWith(l,n),o.promise()}}),x.support=function(t){var n=o.createElement("input"),r=o.createDocumentFragment(),i=o.createElement("div"),s=o.createElement("select"),a=s.appendChild(o.createElement("option"));return n.type?(n.type="checkbox",t.checkOn=""!==n.value,t.optSelected=a.selected,t.reliableMarginRight=!0,t.boxSizingReliable=!0,t.pixelPosition=!1,n.checked=!0,t.noCloneChecked=n.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!a.disabled,n=o.createElement("input"),n.value="t",n.type="radio",t.radioValue="t"===n.value,n.setAttribute("checked","t"),n.setAttribute("name","t"),r.appendChild(n),t.checkClone=r.cloneNode(!0).cloneNode(!0).lastChild.checked,t.focusinBubbles="onfocusin"in e,i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===i.style.backgroundClip,x(function(){var n,r,s="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",a=o.getElementsByTagName("body")[0];a&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",a.appendChild(n).appendChild(i),i.innerHTML="",i.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%",x.swap(a,null!=a.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===i.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(i,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(i,null)||{width:"4px"}).width,r=i.appendChild(o.createElement("div")),r.style.cssText=i.style.cssText=s,r.style.marginRight=r.style.width="0",i.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),a.removeChild(n))}),t):t}({});var L,H,q=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,O=/([A-Z])/g;function F(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=x.expando+Math.random()}F.uid=1,F.accepts=function(e){return e.nodeType?1===e.nodeType||9===e.nodeType:!0},F.prototype={key:function(e){if(!F.accepts(e))return 0;var t={},n=e[this.expando];if(!n){n=F.uid++;try{t[this.expando]={value:n},Object.defineProperties(e,t)}catch(r){t[this.expando]=n,x.extend(e,t)}}return this.cache[n]||(this.cache[n]={}),n},set:function(e,t,n){var r,i=this.key(e),o=this.cache[i];if("string"==typeof t)o[t]=n;else if(x.isEmptyObject(o))x.extend(this.cache[i],t);else for(r in t)o[r]=t[r];return o},get:function(e,t){var n=this.cache[this.key(e)];return t===undefined?n:n[t]},access:function(e,t,n){return t===undefined||t&&"string"==typeof t&&n===undefined?this.get(e,t):(this.set(e,t,n),n!==undefined?n:t)},remove:function(e,t){var n,r,i,o=this.key(e),s=this.cache[o];if(t===undefined)this.cache[o]={};else{x.isArray(t)?r=t.concat(t.map(x.camelCase)):(i=x.camelCase(t),t in s?r=[t,i]:(r=i,r=r in s?[r]:r.match(w)||[])),n=r.length;while(n--)delete s[r[n]]}},hasData:function(e){return!x.isEmptyObject(this.cache[e[this.expando]]||{})},discard:function(e){e[this.expando]&&delete this.cache[e[this.expando]]}},L=new F,H=new F,x.extend({acceptData:F.accepts,hasData:function(e){return L.hasData(e)||H.hasData(e)},data:function(e,t,n){return L.access(e,t,n)},removeData:function(e,t){L.remove(e,t)},_data:function(e,t,n){return H.access(e,t,n)},_removeData:function(e,t){H.remove(e,t)}}),x.fn.extend({data:function(e,t){var n,r,i=this[0],o=0,s=null;if(e===undefined){if(this.length&&(s=L.get(i),1===i.nodeType&&!H.get(i,"hasDataAttrs"))){for(n=i.attributes;n.length>o;o++)r=n[o].name,0===r.indexOf("data-")&&(r=x.camelCase(r.slice(5)),P(i,r,s[r]));H.set(i,"hasDataAttrs",!0)}return s}return"object"==typeof e?this.each(function(){L.set(this,e)}):x.access(this,function(t){var n,r=x.camelCase(e);if(i&&t===undefined){if(n=L.get(i,e),n!==undefined)return n;if(n=L.get(i,r),n!==undefined)return n;if(n=P(i,r,undefined),n!==undefined)return n}else this.each(function(){var n=L.get(this,r);L.set(this,r,t),-1!==e.indexOf("-")&&n!==undefined&&L.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){L.remove(this,e)})}});function P(e,t,n){var r;if(n===undefined&&1===e.nodeType)if(r="data-"+t.replace(O,"-$1").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:q.test(n)?JSON.parse(n):n}catch(i){}L.set(e,t,n)}else n=undefined;return n}x.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=H.get(e,t),n&&(!r||x.isArray(n)?r=H.access(e,t,x.makeArray(n)):r.push(n)),r||[]):undefined},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),s=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire() +},_queueHooks:function(e,t){var n=t+"queueHooks";return H.get(e,n)||H.access(e,n,{empty:x.Callbacks("once memory").add(function(){H.remove(e,[t+"queue",n])})})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),n>arguments.length?x.queue(this[0],e):t===undefined?this:this.each(function(){var n=x.queue(this,e,t);x._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=x.Deferred(),o=this,s=this.length,a=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=undefined),e=e||"fx";while(s--)n=H.get(o[s],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(a));return a(),i.promise(t)}});var R,M,W=/[\t\r\n\f]/g,$=/\r/g,B=/^(?:input|select|textarea|button)$/i;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[x.propFix[e]||e]})},addClass:function(e){var t,n,r,i,o,s=0,a=this.length,u="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,s=0,a=this.length,u=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,i="boolean"==typeof t;return x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,s=0,a=x(this),u=t,l=e.match(w)||[];while(o=l[s++])u=i?u:!a.hasClass(o),a[u?"addClass":"removeClass"](o)}else(n===r||"boolean"===n)&&(this.className&&H.set(this,"__className__",this.className),this.className=this.className||e===!1?"":H.get(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(W," ").indexOf(t)>=0)return!0;return!1},val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=x.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,x(this).val()):e,null==i?i="":"number"==typeof i?i+="":x.isArray(i)&&(i=x.map(i,function(e){return null==e?"":e+""})),t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&t.set(this,i,"value")!==undefined||(this.value=i))});if(i)return t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&(n=t.get(i,"value"))!==undefined?n:(n=i.value,"string"==typeof n?n.replace($,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,s=o?null:[],a=o?i+1:r.length,u=0>i?a:o?i:0;for(;a>u;u++)if(n=r[u],!(!n.selected&&u!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),s=i.length;while(s--)r=i[s],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,t,n){var i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===r?x.prop(e,t,n):(1===s&&x.isXMLDoc(e)||(t=t.toLowerCase(),i=x.attrHooks[t]||(x.expr.match.bool.test(t)?M:R)),n===undefined?i&&"get"in i&&null!==(o=i.get(e,t))?o:(o=x.find.attr(e,t),null==o?undefined:o):null!==n?i&&"set"in i&&(o=i.set(e,n,t))!==undefined?o:(e.setAttribute(t,n+""),n):(x.removeAttr(e,t),undefined))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)&&(e[r]=!1),e.removeAttribute(n)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return o=1!==s||!x.isXMLDoc(e),o&&(t=x.propFix[t]||t,i=x.propHooks[t]),n!==undefined?i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){return e.hasAttribute("tabindex")||B.test(e.nodeName)||e.href?e.tabIndex:-1}}}}),M={set:function(e,t,n){return t===!1?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,t){var n=x.expr.attrHandle[t]||x.find.attr;x.expr.attrHandle[t]=function(e,t,r){var i=x.expr.attrHandle[t],o=r?undefined:(x.expr.attrHandle[t]=undefined)!=n(e,t,r)?t.toLowerCase():null;return x.expr.attrHandle[t]=i,o}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,t){return x.isArray(t)?e.checked=x.inArray(x(e).val(),t)>=0:undefined}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var I=/^key/,z=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,X=/^([^.]*)(?:\.(.+)|)$/;function U(){return!0}function Y(){return!1}function V(){try{return o.activeElement}catch(e){}}x.event={global:{},add:function(e,t,n,i,o){var s,a,u,l,c,p,f,h,d,g,m,y=H.get(e);if(y){n.handler&&(s=n,n=s.handler,o=s.selector),n.guid||(n.guid=x.guid++),(l=y.events)||(l=y.events={}),(a=y.handle)||(a=y.handle=function(e){return typeof x===r||e&&x.event.triggered===e.type?undefined:x.event.dispatch.apply(a.elem,arguments)},a.elem=e),t=(t||"").match(w)||[""],c=t.length;while(c--)u=X.exec(t[c])||[],d=m=u[1],g=(u[2]||"").split(".").sort(),d&&(f=x.event.special[d]||{},d=(o?f.delegateType:f.bindType)||d,f=x.event.special[d]||{},p=x.extend({type:d,origType:m,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&x.expr.match.needsContext.test(o),namespace:g.join(".")},s),(h=l[d])||(h=l[d]=[],h.delegateCount=0,f.setup&&f.setup.call(e,i,g,a)!==!1||e.addEventListener&&e.addEventListener(d,a,!1)),f.add&&(f.add.call(e,p),p.handler.guid||(p.handler.guid=n.guid)),o?h.splice(h.delegateCount++,0,p):h.push(p),x.event.global[d]=!0);e=null}},remove:function(e,t,n,r,i){var o,s,a,u,l,c,p,f,h,d,g,m=H.hasData(e)&&H.get(e);if(m&&(u=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(a=X.exec(t[l])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h){p=x.event.special[h]||{},h=(r?p.delegateType:p.bindType)||h,f=u[h]||[],a=a[2]&&RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=f.length;while(o--)c=f[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(f.splice(o,1),c.selector&&f.delegateCount--,p.remove&&p.remove.call(e,c));s&&!f.length&&(p.teardown&&p.teardown.call(e,d,m.handle)!==!1||x.removeEvent(e,h,m.handle),delete u[h])}else for(h in u)x.event.remove(e,h+t[l],n,r,!0);x.isEmptyObject(u)&&(delete m.handle,H.remove(e,"events"))}},trigger:function(t,n,r,i){var s,a,u,l,c,p,f,h=[r||o],d=y.call(t,"type")?t.type:t,g=y.call(t,"namespace")?t.namespace.split("."):[];if(a=u=r=r||o,3!==r.nodeType&&8!==r.nodeType&&!_.test(d+x.event.triggered)&&(d.indexOf(".")>=0&&(g=d.split("."),d=g.shift(),g.sort()),c=0>d.indexOf(":")&&"on"+d,t=t[x.expando]?t:new x.Event(d,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=g.join("."),t.namespace_re=t.namespace?RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=undefined,t.target||(t.target=r),n=null==n?[t]:x.makeArray(n,[t]),f=x.event.special[d]||{},i||!f.trigger||f.trigger.apply(r,n)!==!1)){if(!i&&!f.noBubble&&!x.isWindow(r)){for(l=f.delegateType||d,_.test(l+d)||(a=a.parentNode);a;a=a.parentNode)h.push(a),u=a;u===(r.ownerDocument||o)&&h.push(u.defaultView||u.parentWindow||e)}s=0;while((a=h[s++])&&!t.isPropagationStopped())t.type=s>1?l:f.bindType||d,p=(H.get(a,"events")||{})[t.type]&&H.get(a,"handle"),p&&p.apply(a,n),p=c&&a[c],p&&x.acceptData(a)&&p.apply&&p.apply(a,n)===!1&&t.preventDefault();return t.type=d,i||t.isDefaultPrevented()||f._default&&f._default.apply(h.pop(),n)!==!1||!x.acceptData(r)||c&&x.isFunction(r[d])&&!x.isWindow(r)&&(u=r[c],u&&(r[c]=null),x.event.triggered=d,r[d](),x.event.triggered=undefined,u&&(r[c]=u)),t.result}},dispatch:function(e){e=x.event.fix(e);var t,n,r,i,o,s=[],a=d.call(arguments),u=(H.get(this,"events")||{})[e.type]||[],l=x.event.special[e.type]||{};if(a[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),t=0;while((i=s[t++])&&!e.isPropagationStopped()){e.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(o.namespace))&&(e.handleObj=o,e.data=o.data,r=((x.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,a),r!==undefined&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,s=[],a=t.delegateCount,u=e.target;if(a&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!==this;u=u.parentNode||this)if(u.disabled!==!0||"click"!==e.type){for(r=[],n=0;a>n;n++)o=t[n],i=o.selector+" ",r[i]===undefined&&(r[i]=o.needsContext?x(i,this).index(u)>=0:x.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&s.push({elem:u,handlers:r})}return t.length>a&&s.push({elem:this,handlers:t.slice(a)}),s},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,s=t.button;return null==e.pageX&&null!=t.clientX&&(n=e.target.ownerDocument||o,r=n.documentElement,i=n.body,e.pageX=t.clientX+(r&&r.scrollLeft||i&&i.scrollLeft||0)-(r&&r.clientLeft||i&&i.clientLeft||0),e.pageY=t.clientY+(r&&r.scrollTop||i&&i.scrollTop||0)-(r&&r.clientTop||i&&i.clientTop||0)),e.which||s===undefined||(e.which=1&s?1:2&s?3:4&s?2:0),e}},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,s=e,a=this.fixHooks[i];a||(this.fixHooks[i]=a=z.test(i)?this.mouseHooks:I.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new x.Event(s),t=r.length;while(t--)n=r[t],e[n]=s[n];return e.target||(e.target=o),3===e.target.nodeType&&(e.target=e.target.parentNode),a.filter?a.filter(e,s):e},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==V()&&this.focus?(this.focus(),!1):undefined},delegateType:"focusin"},blur:{trigger:function(){return this===V()&&this.blur?(this.blur(),!1):undefined},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&x.nodeName(this,"input")?(this.click(),!1):undefined},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==undefined&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)},x.Event=function(e,t){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.getPreventDefault&&e.getPreventDefault()?U:Y):this.type=e,t&&x.extend(this,t),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,undefined):new x.Event(e,t)},x.Event.prototype={isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=U,e&&e.preventDefault&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=U,e&&e.stopPropagation&&e.stopPropagation()},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=U,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,t,n,r,i){var o,s;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=undefined);for(s in e)this.on(s,t,n,e[s],i);return this}if(null==n&&null==r?(r=t,n=t=undefined):null==r&&("string"==typeof t?(r=n,n=undefined):(r=n,n=t,t=undefined)),r===!1)r=Y;else if(!r)return this;return 1===i&&(o=r,r=function(e){return x().off(e),o.apply(this,arguments)},r.guid=o.guid||(o.guid=x.guid++)),this.each(function(){x.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,x(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=undefined),n===!1&&(n=Y),this.each(function(){x.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?x.event.trigger(e,t,n,!0):undefined}});var G=/^.[^:#\[\.,]*$/,J=/^(?:parents|prev(?:Until|All))/,Q=x.expr.match.needsContext,K={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t=x(e,this),n=t.length;return this.filter(function(){var e=0;for(;n>e;e++)if(x.contains(this,t[e]))return!0})},not:function(e){return this.pushStack(et(this,e||[],!0))},filter:function(e){return this.pushStack(et(this,e||[],!1))},is:function(e){return!!et(this,"string"==typeof e&&Q.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],s=Q.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(s?s.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?g.call(x(e),this[0]):g.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function Z(e,t){while((e=e[t])&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return Z(e,"nextSibling")},prev:function(e){return Z(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return e.contentDocument||x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(K[e]||x.unique(i),J.test(e)&&i.reverse()),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,t,n){var r=[],i=n!==undefined;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function et(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(G.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return g.call(t,e)>=0!==n})}var tt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,nt=/<([\w:]+)/,rt=/<|&#?\w+;/,it=/<(?:script|style|link)/i,ot=/^(?:checkbox|radio)$/i,st=/checked\s*(?:[^=]|=\s*.checked.)/i,at=/^$|\/(?:java|ecma)script/i,ut=/^true\/(.*)/,lt=/^\s*\s*$/g,ct={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ct.optgroup=ct.option,ct.tbody=ct.tfoot=ct.colgroup=ct.caption=ct.thead,ct.th=ct.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===undefined?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(mt(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&dt(mt(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(mt(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!it.test(e)&&!ct[(nt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(tt,"<$1>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(x.cleanData(mt(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=f.apply([],e);var r,i,o,s,a,u,l=0,c=this.length,p=this,h=c-1,d=e[0],g=x.isFunction(d);if(g||!(1>=c||"string"!=typeof d||x.support.checkClone)&&st.test(d))return this.each(function(r){var i=p.eq(r);g&&(e[0]=d.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(r=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),i=r.firstChild,1===r.childNodes.length&&(r=i),i)){for(o=x.map(mt(r,"script"),ft),s=o.length;c>l;l++)a=r,l!==h&&(a=x.clone(a,!0,!0),s&&x.merge(o,mt(a,"script"))),t.call(this[l],a,l);if(s)for(u=o[o.length-1].ownerDocument,x.map(o,ht),l=0;s>l;l++)a=o[l],at.test(a.type||"")&&!H.access(a,"globalEval")&&x.contains(u,a)&&(a.src?x._evalUrl(a.src):x.globalEval(a.textContent.replace(lt,"")))}return this}}),x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=[],i=x(e),o=i.length-1,s=0;for(;o>=s;s++)n=s===o?this:this.clone(!0),x(i[s])[t](n),h.apply(r,n.get());return this.pushStack(r)}}),x.extend({clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=x.contains(e.ownerDocument,e);if(!(x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(s=mt(a),o=mt(e),r=0,i=o.length;i>r;r++)yt(o[r],s[r]);if(t)if(n)for(o=o||mt(e),s=s||mt(a),r=0,i=o.length;i>r;r++)gt(o[r],s[r]);else gt(e,a);return s=mt(a,"script"),s.length>0&&dt(s,!u&&mt(e,"script")),a},buildFragment:function(e,t,n,r){var i,o,s,a,u,l,c=0,p=e.length,f=t.createDocumentFragment(),h=[];for(;p>c;c++)if(i=e[c],i||0===i)if("object"===x.type(i))x.merge(h,i.nodeType?[i]:i);else if(rt.test(i)){o=o||f.appendChild(t.createElement("div")),s=(nt.exec(i)||["",""])[1].toLowerCase(),a=ct[s]||ct._default,o.innerHTML=a[1]+i.replace(tt,"<$1>")+a[2],l=a[0];while(l--)o=o.firstChild;x.merge(h,o.childNodes),o=f.firstChild,o.textContent=""}else h.push(t.createTextNode(i));f.textContent="",c=0;while(i=h[c++])if((!r||-1===x.inArray(i,r))&&(u=x.contains(i.ownerDocument,i),o=mt(f.appendChild(i),"script"),u&&dt(o),n)){l=0;while(i=o[l++])at.test(i.type||"")&&n.push(i)}return f},cleanData:function(e){var t,n,r,i,o,s,a=x.event.special,u=0;for(;(n=e[u])!==undefined;u++){if(F.accepts(n)&&(o=n[H.expando],o&&(t=H.cache[o]))){if(r=Object.keys(t.events||{}),r.length)for(s=0;(i=r[s])!==undefined;s++)a[i]?x.event.remove(n,i):x.removeEvent(n,i,t.handle);H.cache[o]&&delete H.cache[o]}delete L.cache[n[L.expando]]}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}});function pt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function ft(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function ht(e){var t=ut.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function dt(e,t){var n=e.length,r=0;for(;n>r;r++)H.set(e[r],"globalEval",!t||H.get(t[r],"globalEval"))}function gt(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(H.hasData(e)&&(o=H.access(e),s=H.set(t,o),l=o.events)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;r>n;n++)x.event.add(t,i,l[i][n])}L.hasData(e)&&(a=L.access(e),u=x.extend({},a),L.set(t,u))}}function mt(e,t){var n=e.getElementsByTagName?e.getElementsByTagName(t||"*"):e.querySelectorAll?e.querySelectorAll(t||"*"):[];return t===undefined||t&&x.nodeName(e,t)?x.merge([e],n):n}function yt(e,t){var n=t.nodeName.toLowerCase();"input"===n&&ot.test(e.type)?t.checked=e.checked:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}x.fn.extend({wrapAll:function(e){var t;return x.isFunction(e)?this.each(function(t){x(this).wrapAll(e.call(this,t))}):(this[0]&&(t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var vt,xt,bt=/^(none|table(?!-c[ea]).+)/,wt=/^margin/,Tt=RegExp("^("+b+")(.*)$","i"),Ct=RegExp("^("+b+")(?!px)[a-z%]+$","i"),kt=RegExp("^([+-])=("+b+")","i"),Nt={BODY:"block"},Et={position:"absolute",visibility:"hidden",display:"block"},St={letterSpacing:0,fontWeight:400},jt=["Top","Right","Bottom","Left"],Dt=["Webkit","O","Moz","ms"];function At(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Dt.length;while(i--)if(t=Dt[i]+n,t in e)return t;return r}function Lt(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function Ht(t){return e.getComputedStyle(t,null)}function qt(e,t){var n,r,i,o=[],s=0,a=e.length;for(;a>s;s++)r=e[s],r.style&&(o[s]=H.get(r,"olddisplay"),n=r.style.display,t?(o[s]||"none"!==n||(r.style.display=""),""===r.style.display&&Lt(r)&&(o[s]=H.access(r,"olddisplay",Rt(r.nodeName)))):o[s]||(i=Lt(r),(n&&"none"!==n||!i)&&H.set(r,"olddisplay",i?n:x.css(r,"display"))));for(s=0;a>s;s++)r=e[s],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[s]||"":"none"));return e}x.fn.extend({css:function(e,t){return x.access(this,function(e,t,n){var r,i,o={},s=0;if(x.isArray(t)){for(r=Ht(e),i=t.length;i>s;s++)o[t[s]]=x.css(e,t[s],!1,r);return o}return n!==undefined?x.style(e,t,n):x.css(e,t)},e,t,arguments.length>1)},show:function(){return qt(this,!0)},hide:function(){return qt(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:Lt(this))?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=vt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,s,a=x.camelCase(t),u=e.style;return t=x.cssProps[a]||(x.cssProps[a]=At(u,a)),s=x.cssHooks[t]||x.cssHooks[a],n===undefined?s&&"get"in s&&(i=s.get(e,!1,r))!==undefined?i:u[t]:(o=typeof n,"string"===o&&(i=kt.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(x.css(e,t)),o="number"),null==n||"number"===o&&isNaN(n)||("number"!==o||x.cssNumber[a]||(n+="px"),x.support.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),s&&"set"in s&&(n=s.set(e,n,r))===undefined||(u[t]=n)),undefined)}},css:function(e,t,n,r){var i,o,s,a=x.camelCase(t);return t=x.cssProps[a]||(x.cssProps[a]=At(e.style,a)),s=x.cssHooks[t]||x.cssHooks[a],s&&"get"in s&&(i=s.get(e,!0,n)),i===undefined&&(i=vt(e,t,r)),"normal"===i&&t in St&&(i=St[t]),""===n||n?(o=parseFloat(i),n===!0||x.isNumeric(o)?o||0:i):i}}),vt=function(e,t,n){var r,i,o,s=n||Ht(e),a=s?s.getPropertyValue(t)||s[t]:undefined,u=e.style;return s&&(""!==a||x.contains(e.ownerDocument,e)||(a=x.style(e,t)),Ct.test(a)&&wt.test(t)&&(r=u.width,i=u.minWidth,o=u.maxWidth,u.minWidth=u.maxWidth=u.width=a,a=s.width,u.width=r,u.minWidth=i,u.maxWidth=o)),a};function Ot(e,t,n){var r=Tt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function Ft(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,s=0;for(;4>o;o+=2)"margin"===n&&(s+=x.css(e,n+jt[o],!0,i)),r?("content"===n&&(s-=x.css(e,"padding"+jt[o],!0,i)),"margin"!==n&&(s-=x.css(e,"border"+jt[o]+"Width",!0,i))):(s+=x.css(e,"padding"+jt[o],!0,i),"padding"!==n&&(s+=x.css(e,"border"+jt[o]+"Width",!0,i)));return s}function Pt(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Ht(e),s=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=vt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Ct.test(i))return i;r=s&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+Ft(e,t,n||(s?"border":"content"),r,o)+"px"}function Rt(e){var t=o,n=Nt[e];return n||(n=Mt(e,t),"none"!==n&&n||(xt=(xt||x("