commit 767fb638ce7aeea3bbc5aa28e957778fb257a0cc Author: Michatec Date: Sat Sep 27 20:49:58 2025 +0200 Files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d31ce3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +__pycache__ +instance +*.pyc +*.pyo +*.pyd +migrations +*.sqlite3 +*.log +*.db +*.env +*.DS_Store +.vscode +routes/__pycache__ +tools +*.pot +*.mo +routes/oauth.py +static/profile_pics +static/uploads +commands.txt +py-to-exemfc.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d80e8ff --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# MiniFacebook + +MiniFacebook is a minimalist social network built with [Flask](https://flask.palletsprojects.com/), [SQLAlchemy](https://www.sqlalchemy.org/), and [Bootstrap](https://getbootstrap.com/). It allows you to share posts, images, videos, and messages with friends—ad-free and privacy-focused. + +## Features + +- Share posts, images, videos, and documents +- Friend requests and friends list +- Activity notifications +- Shop for premium features (e.g., gold frames, extra uploads) +- Admin panel with user management +- Multilingual (German/English) +- Dark and light mode +- Discord login and linking +- Support ticket system +- Password reset via email +- Responsive design for desktop and mobile + +## Installation + +1. **Clone the repository** + + ```sh + git clone https://github.com/Michatec/MiniFaceBook.git + cd MiniFaceBook + ``` + +2. **Install dependencies** + + ```sh + pip install -r requirments.txt + ``` + +3. **Start** + + ```sh + python main.py + ``` + +## Help to translate + + + +## License + +This project is licensed under the GNU General Public License v3.0. diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..fdff2e5 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,2 @@ +[python: **.py] +[jinja2: templates/**.html] \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..3017a00 --- /dev/null +++ b/main.py @@ -0,0 +1,224 @@ +from flask import Flask, request, render_template, redirect, url_for, flash, abort, jsonify +from flask_migrate import Migrate +from flask_login import LoginManager, login_required, current_user +from werkzeug.security import generate_password_hash +from flask_babel import Babel, gettext as _ +from waitress import serve +from routes.admin import admin_bp +from routes.discord import discord_bp +from routes.post import post_bp +from routes.login import log_bp +from routes.support import support_bp +from routes.like import like_bp +from routes.profile import profile_bp +from routes.user import user_bp +from routes.friends import friends_bp +from routes.notifications import noti_bp +from routes.credits import credits_bp +from models import db, User, Reward, Event, UserShopItem, ShopItem, SHOPITEM_ID_PREMIUM, SHOPITEM_ID_GOLDRAHMEN, SHOPITEM_ID_EXTRA_TYPES, SHOPITEM_ID_EXTRA_UPLOAD +from routes.oauth import oauth +import re +import os + +__mapper_args__ = {"confirm_deleted_rows": False} + +app = Flask(__name__) +app.config['SECRET_KEY'] = "secret_key" +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['BABEL_DEFAULT_LOCALE'] = 'en' +db.init_app(app) +migrate = Migrate(app, db) +app_login = LoginManager(app) +app_login.login_view = 'log.login' +app_login.login_message = _('Please log in to access this page.') +app_login.login_message_category = 'info' +babel = Babel(app) +oauth.init_app(app) + +if not os.path.exists('instance/site.db'): + with app.app_context(): + db.create_all() + +if not os.path.exists('static/uploads'): + os.makedirs('static/uploads') +if not os.path.exists('static/profile_pics'): + os.makedirs('static/profile_pics') + +app.register_blueprint(admin_bp) +app.register_blueprint(discord_bp) +app.register_blueprint(post_bp) +app.register_blueprint(log_bp) +app.register_blueprint(support_bp) +app.register_blueprint(profile_bp) +app.register_blueprint(like_bp) +app.register_blueprint(user_bp) +app.register_blueprint(friends_bp) +app.register_blueprint(noti_bp) +app.register_blueprint(credits_bp) + +with app.app_context(): + if db.session.query(ShopItem).count() == 0: + db.session.add(ShopItem( + name="Premium Account", + description="Exclusive features and content.", + price=100, + icon="bi-star" + )) + db.session.add(ShopItem( + name="Gold Profile Frame", + description="Adds a golden profile frame to your profile.", + price=50, + icon="bi-person-bounding-box" + )) + db.session.add(ShopItem( + name="Extra Upload Slot", + description="Become able to upload more files.", + price=130, + icon="bi-cloud-upload" + )) + db.session.add(ShopItem( + name="More Types", + description="More types for your posts. Limit: 500 types per post.", + price=80, + icon="bi-megaphone" + )) + db.session.commit() + else: + pass + +def get_locale(): + lang = request.cookies.get('lang') + if lang in ['de', 'en']: + return lang + +babel.init_app(app, locale_selector=get_locale) + +def needs_admin_setup(): + return db.session.query(User).filter_by(is_admin=True).count() == 0 + +@app.context_processor +def inject_user(): + return dict(user=current_user if current_user.is_authenticated else None) + +@app.context_processor +def inject_theme(): + theme = request.cookies.get('theme') + if not theme: + theme = 'dark' + return dict(theme_class=f"{theme}-mode" if theme else "") + +@app.context_processor +def inject_locale(): + return dict(get_locale=get_locale) + +@app.context_processor +def inject_shopitem_ids(): + return dict( + SHOPITEM_ID_GOLDRAHMEN=SHOPITEM_ID_GOLDRAHMEN, + SHOPITEM_ID_PREMIUM=SHOPITEM_ID_PREMIUM, + SHOPITEM_ID_EXTRA_UPLOAD=SHOPITEM_ID_EXTRA_UPLOAD, + SHOPITEM_ID_EXTRA_TYPES=SHOPITEM_ID_EXTRA_TYPES + ) + +@app_login.user_loader +def load_user(user_id): + return db.session.get(User, int(user_id)) + +@app.before_request +def check_for_admin(): + allowed_routes = ['setup', 'static'] + if needs_admin_setup() and request.endpoint not in allowed_routes: + return redirect(url_for('setup')) + +@app.route('/', methods=['GET']) +def index(): + if current_user.is_authenticated: + return redirect(url_for('post.feed')) + return render_template('index.html') + + +@app.route('/setup', methods=['GET', 'POST']) +def setup(): + if not needs_admin_setup(): + return redirect(url_for('log.login')) + if request.method == 'POST': + username = request.form['username'] + email = request.form['email'] + password = request.form['password'] + confirm_password = request.form['confirm_password'] + if password != confirm_password: + flash(_('Passwords do not match.'), 'danger') + elif db.session.query(User).filter_by(username=username).first(): + flash(_('Username already exists.'), 'danger') + elif db.session.query(User).filter_by(email=email).first(): + flash(_('E-Mail already exists.'), 'danger') + elif len(password) < 8: + flash(_('Password must be at least 8 characters long.'), 'danger') + elif not re.match(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', email): + flash(_('Invalid email address.'), 'danger') + elif not re.match(r'^[a-zA-Z0-9_.+-]+$', username): + flash(_('Invalid username. Only alphanumeric characters are allowed.'), 'danger') + else: + hashed_password = generate_password_hash(password, method='pbkdf2:sha256') + admin_user = User(username=username, email=email, password=hashed_password, is_admin=True, is_owner=True) + db.session.add(admin_user) + db.session.commit() + flash(_('Admin account created. You can now log in.'), 'success') + return redirect(url_for('log.login')) + return render_template('setup.html') + +@app.route('/api/events') +@login_required +def api_events(): + if not current_user.is_admin: + abort(403) + + events = db.session.query(Event).order_by(Event.timestamp.desc()).limit(20).all() + return jsonify([ + {"timestamp": e.timestamp.strftime('%Y-%m-%d %H:%M'), "message": e.message} + for e in events + ]) + +@app.route('/shop', methods=['GET', 'POST']) +@login_required +def shop(): + items = db.session.query(ShopItem).all() + message = None + owned_ids = [usi.item_id for usi in current_user.shop_items] + if request.method == 'POST': + item_id = int(request.form['item_id']) + item = db.session.get(ShopItem, item_id) + if item_id in owned_ids: + message = _("Already purchased!") + elif item and current_user.reward_points() >= item.price: + db.session.add(Reward(user_id=current_user.id, type=f'buy_{item.name}', points=-item.price)) + db.session.add(UserShopItem(user_id=current_user.id, item_id=item.id)) + db.session.commit() + message = _(f"Purchased: {item.name}") + owned_ids.append(item_id) + else: + message = _("Not enough points!") + return render_template('shop.html', items=items, message=message, owned_ids=owned_ids) + +@app.errorhandler(403) +def forbidden(error): + return render_template('403.html'), 403 + +@app.errorhandler(404) +def not_found(error): + flash(f'{error}', 'danger') + if current_user.is_authenticated: + return redirect(url_for('post.feed')) + return render_template('index.html'), 200 + +@app.route('/secret') +@login_required +def secret(): + return render_template('secret.html') + +if __name__ == '__main__': + try: + serve(app, host="0.0.0.0", port=80, threads=12) + except: + app.run(debug=True, host="0.0.0.0", port=80) \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..aff87d8 --- /dev/null +++ b/models.py @@ -0,0 +1,137 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_login import UserMixin +from datetime import datetime + +db = SQLAlchemy() + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(150), unique=True, nullable=False) + email = db.Column(db.String(150), unique=True, nullable=False) + password = db.Column(db.String(150), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.now) + is_admin = db.Column(db.Boolean, default=False) + is_owner = db.Column(db.Boolean, default=False) + profile_pic = db.Column(db.String(200), default='default.png') + discord_id = db.Column(db.String(50), unique=True) + discord_linked = db.Column(db.Boolean, default=False) + posts = db.relationship('Post', backref='user', cascade="all, delete", passive_deletes=True) + comments = db.relationship('Comment', backref='user', cascade="all, delete", passive_deletes=True) + likes = db.relationship('Like', backref='user', cascade="all, delete", passive_deletes=True) + friendships_sent = db.relationship( + 'Friendship', + foreign_keys='Friendship.requester_id', + backref='requester', + cascade="all, delete", + passive_deletes=True + ) + friendships_received = db.relationship( + 'Friendship', + foreign_keys='Friendship.receiver_id', + backref='receiver', + cascade="all, delete", + passive_deletes=True + ) + uploads = db.relationship('Upload', backref='user', cascade="all, delete", passive_deletes=True) + rewards = db.relationship('Reward', backref='user', cascade="all, delete", passive_deletes=True) + shop_items = db.relationship('UserShopItem', backref='user', cascade="all, delete", passive_deletes=True) + + def reward_points(self): + return sum(r.points for r in self.rewards) + +class Friendship(db.Model): + id = db.Column(db.Integer, primary_key=True) + requester_id = db.Column(db.Integer, db.ForeignKey('user.id')) + receiver_id = db.Column(db.Integer, db.ForeignKey('user.id')) + status = db.Column(db.String(20), default='pending') + +class Post(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + content = db.Column(db.Text, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.now) + visibility = db.Column(db.String(20), default='public') + likes = db.relationship('Like', backref='post', lazy='dynamic', cascade="all, delete-orphan", passive_deletes=True) + comments = db.relationship('Comment', backref='post', cascade="all, delete-orphan", passive_deletes=True) + uploads = db.relationship('Upload', backref='post', cascade="all, delete-orphan", passive_deletes=True) + +class Comment(db.Model): + id = db.Column(db.Integer, primary_key=True) + post_id = db.Column(db.Integer, db.ForeignKey('post.id')) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + content = db.Column(db.Text, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.now) + +class Like(db.Model): + id = db.Column(db.Integer, primary_key=True) + post_id = db.Column(db.Integer, db.ForeignKey('post.id', ondelete='CASCADE')) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + +class PasswordResetRequest(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + requested_at = db.Column(db.DateTime, default=datetime.now) + status = db.Column(db.String(20), default='pending') + user = db.relationship('User', backref='reset_requests') + +class Upload(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + post_id = db.Column(db.Integer, db.ForeignKey('post.id')) + filename = db.Column(db.String(255)) + filetype = db.Column(db.String(50)) + uploaded_at = db.Column(db.DateTime, default=datetime.now) + +class Event(db.Model): + id = db.Column(db.Integer, primary_key=True) + timestamp = db.Column(db.DateTime, default=datetime.now) + message = db.Column(db.String(255)) + +class Notification(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + message = db.Column(db.String(255)) + created_at = db.Column(db.DateTime, default=datetime.now) + read = db.Column(db.Boolean, default=False) + +class Reward(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + type = db.Column(db.String(50)) + points = db.Column(db.Integer, default=0) + created_at = db.Column(db.DateTime, default=datetime.now) + +class ShopItem(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + description = db.Column(db.String(255)) + price = db.Column(db.Integer, nullable=False) + icon = db.Column(db.String(50), default="bi-gift") + +class UserShopItem(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + item_id = db.Column(db.Integer, db.ForeignKey('shop_item.id')) + bought_at = db.Column(db.DateTime, default=datetime.now) + item = db.relationship('ShopItem') + +class SupportRequest(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + title = db.Column(db.String(100), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.now) + status = db.Column(db.String(20), default='open') + comments = db.relationship('SupportComment', backref='request', cascade="all, delete", passive_deletes=True) + +class SupportComment(db.Model): + id = db.Column(db.Integer, primary_key=True) + request_id = db.Column(db.Integer, db.ForeignKey('support_request.id')) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + message = db.Column(db.Text, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.now) + user = db.relationship('User') + +SHOPITEM_ID_GOLDRAHMEN = 2 +SHOPITEM_ID_PREMIUM = 1 +SHOPITEM_ID_EXTRA_UPLOAD = 3 +SHOPITEM_ID_EXTRA_TYPES = 4 \ No newline at end of file diff --git a/requirments.txt b/requirments.txt new file mode 100644 index 0000000..ecf3bb6 --- /dev/null +++ b/requirments.txt @@ -0,0 +1,8 @@ +flask +flask_login +flask_migrate +werkzeug +flask_babel +waitress +authlib +sqlalchemy \ No newline at end of file diff --git a/routes/admin.py b/routes/admin.py new file mode 100644 index 0000000..1c39bfc --- /dev/null +++ b/routes/admin.py @@ -0,0 +1,330 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, abort, request +from flask_login import login_required, current_user +from models import db, User, Post, Friendship, Comment, Upload, Notification, Event, PasswordResetRequest, Like, UserShopItem, Reward, SupportComment, SupportRequest +from werkzeug.security import generate_password_hash +from flask_babel import gettext as _ +import os + +__mapper_args__ = {"confirm_deleted_rows": False} +admin_bp = Blueprint('admin', __name__, url_prefix='/admin') + +@admin_bp.route('/reset_requests/delete_all', methods=['GET', 'POST']) +@login_required +def admin_delete_all_reset_requests(): + if not current_user.is_admin: + abort(403) + db.session.query(PasswordResetRequest).delete() + db.session.commit() + flash(_('All password reset requests have been deleted.'), 'success') + return redirect(url_for('admin.reset_requests')) + +@admin_bp.route('/') +@login_required +def admin(): + if not current_user.is_admin: + abort(403) + users = db.session.query(User).all() + posts = db.session.query(Post).all() + friendships = db.session.query(Friendship).all() + comments = db.session.query(Comment).all() + uploads = db.session.query(Upload).all() + all_notifications = db.session.query(Notification).order_by(Notification.created_at.desc()).all() + events = db.session.query(Event).order_by(Event.timestamp.desc()).limit(50).all() + user_shop_items = db.session.query(UserShopItem).all() + return render_template( + 'admin.html', + users=users, + posts=posts, + friendships=friendships, + comments=comments, + uploads=uploads, + all_notifications=all_notifications, + events=events, + user_shop_items=user_shop_items + ) + +@admin_bp.route('/reset_requests') +@login_required +def reset_requests(): + if not current_user.is_admin: + abort(403) + requests = db.session.query(PasswordResetRequest).filter_by(status='pending').all() + requests_done = db.session.query(PasswordResetRequest).filter_by(status='done').all() + requests_rejected = db.session.query(PasswordResetRequest).filter_by(status='rejected').all() + return render_template('reset_requests.html', requests=requests, requests_done=requests_done, requests_rejected=requests_rejected) + +@admin_bp.route('/reset_requests//reject', methods=['POST']) +@login_required +def reject_reset_request(req_id): + if not current_user.is_admin: + abort(403) + req = db.session.get(PasswordResetRequest, req_id) + if req: + req.status = 'rejected' + db.session.commit() + flash(_('Request rejected.'), 'info') + return redirect(url_for('admin.reset_requests')) + +@admin_bp.route('/reset_requests//reset', methods=['GET', 'POST']) +@login_required +def admin_reset_password(req_id): + if not current_user.is_admin: + abort(403) + req = db.session.get(PasswordResetRequest, req_id) + if req and req.status == 'pending': + user = db.session.get(User, req.user_id) + if request.method == 'POST': + new_pw = request.form['new_password'] + if not new_pw or len(new_pw) < 4: + flash(_('Password too short.'), 'danger') + else: + user.password = generate_password_hash(new_pw, method='pbkdf2:sha256') + req.status = 'done' + db.session.commit() + flash(_(f'Password for {user.username} reset.'), 'success') + return redirect(url_for('admin.reset_requests')) + return render_template('admin_set_password.html', req=req, user=user) + return redirect(url_for('admin.reset_requests')) + +@admin_bp.route('/delete_post/', methods=['POST']) +@login_required +def admin_delete_post(post_id): + if not current_user.is_admin: + abort(403) + post = db.session.get(Post, post_id) + if post: + for upload in post.uploads: + file_path = os.path.join('static/uploads', upload.filename) + try: + if os.path.exists(file_path): + os.remove(file_path) + except Exception: + pass + likes = db.session.query(Like).filter_by(post_id=post_id).all() + for like in likes: + db.session.delete(like) + comments = db.session.query(Comment).filter_by(post_id=post_id).all() + for comment in comments: + db.session.delete(comment) + db.session.delete(post) + event = Event(message=_(f"Admin {current_user.username} has deleted post {post.id}.")) + db.session.add(event) + notification = Notification(message=_(f"Your post {post.id} has been deleted by an admin."), user_id=post.user_id) + db.session.add(notification) + db.session.commit() + flash(_('Post and associated files deleted.'), 'success') + return redirect(url_for('admin.admin')) + +@admin_bp.route('/delete_user/', methods=['POST']) +@login_required +def admin_delete_user(user_id): + user = db.session.get(User, user_id) + if user.is_owner: + flash(_('Cannot delete the owner account.'), 'danger') + return redirect(url_for('admin.admin')) + if user and not user.is_admin: + event = Event(message=f"Admin {current_user.username} hat {user.username} gelöscht.") + db.session.add(event) + for post in user.posts: + db.session.delete(post) + for friendship in user.friendships_sent + user.friendships_received: + db.session.delete(friendship) + for comment in user.comments: + db.session.delete(comment) + for user_shop_item in user.shop_items: + db.session.delete(user_shop_item) + for reward in user.rewards: + db.session.delete(reward) + for like in user.likes: + db.session.delete(like) + for upload in user.uploads: + file_path = os.path.join('static/uploads', upload.filename) + try: + if os.path.exists(file_path): + os.remove(file_path) + except Exception: + pass + db.session.delete(upload) + if user.profile_pic and not user.profile_pic == 'default.png': + try: + os.remove(os.path.join('static/profile_pics', user.profile_pic)) + except Exception: + pass + notifications = db.session.query(Notification).filter_by(user_id=user.id).all() + for notif in notifications: + db.session.delete(notif) + db.session.delete(user) + db.session.commit() + flash(_('User deleted.'), 'success') + else: + flash(_('Cannot delete admin or user not found.'), 'danger') + return redirect(url_for('admin.admin')) + +@admin_bp.route('/delete_pic/', methods=['POST']) +@login_required +def admin_delete_pic(user_id): + if not current_user.is_admin: + abort(403) + user = db.session.get(User, user_id) + if user and user.profile_pic and user.profile_pic != 'default.png': + try: + os.remove(os.path.join('static/profile_pics', user.profile_pic)) + except Exception: + pass + user.profile_pic = "default.png" + db.session.commit() + flash(_(f'Profile picture of {user.username} deleted.'), 'success') + return redirect(url_for('admin.admin')) + +@admin_bp.route('/delete_all_notifications', methods=['POST']) +@login_required +def admin_delete_all_notifications(): + if not current_user.is_admin: + abort(403) + db.session.query(Notification).delete() + db.session.commit() + flash(_('All notifications have been deleted.'), 'success') + return redirect(url_for('admin.admin')) + +@admin_bp.route('/delete_all_events', methods=['POST']) +@login_required +def admin_delete_all_events(): + if not current_user.is_admin: + abort(403) + db.session.query(Event).delete() + db.session.commit() + flash(_('All events have been deleted.'), 'success') + return redirect(url_for('admin.admin')) + +@admin_bp.route('/delete_upload/', methods=['POST']) +@login_required +def admin_delete_upload(upload_id): + if not current_user.is_admin: + abort(403) + upload = db.session.get(Upload, upload_id) + if upload: + file_path = os.path.join('static/uploads', upload.filename) + try: + if os.path.exists(file_path): + os.remove(file_path) + except Exception: + pass + db.session.delete(upload) + db.session.commit() + flash(_('Upload deleted.'), 'success') + return redirect(url_for('admin.admin')) + +@admin_bp.route('/delete_all_uploads', methods=['POST']) +@login_required +def admin_delete_all_uploads(): + if not current_user.is_admin: + abort(403) + db.session.query(Upload).delete() + db.session.commit() + upload_dir = 'static/uploads' + for filename in os.listdir(upload_dir): + file_path = os.path.join(upload_dir, filename) + try: + if os.path.isfile(file_path): + os.remove(file_path) + except Exception: + pass + flash(_('All uploads have been deleted.'), 'success') + return redirect(url_for('admin.admin')) + +@admin_bp.route('/admin/points/', methods=['POST']) +@login_required +def admin_points(user_id): + if not current_user.is_admin: + abort(403) + action = request.form.get('action') + try: + points = int(request.form['points']) + except: + flash(_('No Points entered!')) + return redirect(url_for('admin.admin')) + cuser = db.session.get(User, current_user.id) + if not cuser.is_owner: + abort(403) + if action == 'add': + db.session.add(Reward(user_id=user_id, type='admin', points=points)) + db.session.commit() + flash(_('Points added!'), 'success') + elif action == 'remove': + user = db.session.get(User, user_id) + if user.reward_points() >= points: + db.session.add(Reward(user_id=user_id, type='admin', points=-points)) + db.session.commit() + flash(_('Points removed!'), 'success') + else: + flash(_("The user has not enough points to take!"), 'danger') + return redirect(url_for('admin.admin')) + +@admin_bp.route('/make_admin/', methods=['POST']) +@login_required +def make_admin(user_id): + if not current_user.is_admin: + abort(403) + user = db.session.get(User, user_id) + if user and not user.is_admin: + user.is_admin = True + db.session.commit() + flash(_(f"{user.username} is now an admin."), "success") + return redirect(url_for('admin.admin')) + +@admin_bp.route('/remove_admin/', methods=['POST']) +@login_required +def remove_admin(user_id): + if not current_user.is_admin: + abort(403) + user = db.session.get(User, user_id) + if user and user.is_admin and not user.is_owner: + user.is_admin = False + db.session.commit() + flash(_(f"Admin rights of {user.username} removed."), "info") + else: + flash(_("Owner cannot be removed!"), "danger") + return redirect(url_for('admin.admin')) + +@admin_bp.route('/wipe_server', methods=['POST']) +@login_required +def wipe_server(): + if not current_user.is_admin and not current_user.is_owner: + abort(403) + + db.session.query(Reward).delete() + db.session.query(UserShopItem).delete() + db.session.query(Like).delete() + db.session.query(Comment).delete() + db.session.query(Friendship).delete() + db.session.query(Post).delete() + db.session.query(Upload).delete() + db.session.query(Notification).delete() + db.session.query(Event).delete() + db.session.query(PasswordResetRequest).delete() + db.session.query(User).delete() + db.session.query(SupportComment).delete() + db.session.query(SupportRequest).delete() + db.session.commit() + + upload_dir = 'static/uploads' + for filename in os.listdir(upload_dir): + file_path = os.path.join(upload_dir, filename) + try: + if os.path.isfile(file_path): + os.remove(file_path) + except Exception: + pass + + profile_dir = 'static/profile_pics' + for filename in os.listdir(profile_dir): + if filename != 'default.png': + file_path = os.path.join(profile_dir, filename) + try: + if os.path.isfile(file_path): + os.remove(file_path) + except Exception: + pass + + flash(_('All Data has been deleted.'), 'success') + return redirect(url_for('admin.admin')) \ No newline at end of file diff --git a/routes/credits.py b/routes/credits.py new file mode 100644 index 0000000..da7e621 --- /dev/null +++ b/routes/credits.py @@ -0,0 +1,14 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request +from flask_login import login_required, current_user +from models import db +from flask_babel import gettext as _ + +credits_bp = Blueprint('credit', __name__) + +@credits_bp.route('/credits') +def credits(): + return render_template('credits.html') + +@credits_bp.route('/privacy-policy') +def privacy_policy(): + return render_template('privacy_policy.html') \ No newline at end of file diff --git a/routes/discord.py b/routes/discord.py new file mode 100644 index 0000000..f1b8af2 --- /dev/null +++ b/routes/discord.py @@ -0,0 +1,87 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request +from flask_login import login_required, current_user +from models import db, User +from werkzeug.security import generate_password_hash +from flask_babel import gettext as _ +from routes.oauth import discord +from routes.login import login_user + +discord_bp = Blueprint('discord', __name__) + +@discord_bp.route('/login/discord') +def login_discord(): + redirect_uri = url_for('discord.discord_login_callback', _external=True) + return discord.authorize_redirect(redirect_uri) + +@discord_bp.route('/login/discord/callback', methods=['GET', 'POST']) +def discord_login_callback(): + if request.method == 'GET': + token = discord.authorize_access_token() + user_data = discord.get('users/@me').json() + user = User.query.filter_by(discord_id=user_data['id']).first() + if user: + login_user(user) + flash(_('Logged in with Discord.'), 'success') + return redirect(url_for('post.feed')) + else: + flash(_('No account linked with this Discord. Please register.'), 'info') + return render_template( + 'discord_register.html', + username=user_data['username'], + email=user_data.get('email', ''), + discord_id=user_data['id'] + ) + else: + username = request.form.get('username') + email = request.form.get('email') + discord_id = request.form.get('discord_id') + password = request.form.get('password') + confirm_password = request.form.get('confirm_password') + if not password or len(password) < 8: + flash(_('Password must be at least 8 characters long.'), 'danger') + return render_template('discord_register.html', username=username, email=email, discord_id=discord_id) + if password != confirm_password: + flash(_('Passwords do not match.'), 'danger') + return render_template('discord_register.html', username=username, email=email, discord_id=discord_id) + if db.session.query(User).filter_by(username=username).first(): + flash(_('Username already exists. Please Report It.'), 'danger') + return render_template('discord_register.html', username="", email=email, discord_id=discord_id) + hashed_password = generate_password_hash(password, method='pbkdf2:sha256') + new_user = User( + username=username, + email=email, + password=hashed_password, + discord_id=discord_id, + discord_linked=True + ) + db.session.add(new_user) + db.session.commit() + login_user(new_user) + flash(_('Account created and logged in with Discord.'), 'success') + return redirect(url_for('post.feed')) + +@discord_bp.route('/link_discord') +@login_required +def link_discord(): + redirect_uri = url_for('discord.authorize_discord', _external=True) + return discord.authorize_redirect(redirect_uri) + +@discord_bp.route('/authorize/discord') +@login_required +def authorize_discord(): + token = discord.authorize_access_token() + user_data = discord.get('users/@me').json() + current_user.discord_id = user_data['id'] + current_user.discord_linked = True + db.session.commit() + flash(_('Discord account linked!'), 'success') + return redirect(url_for('profil.profile')) + +@discord_bp.route('/unlink_discord', methods=['POST']) +@login_required +def unlink_discord(): + current_user.discord_id = None + current_user.discord_linked = False + db.session.commit() + flash(_('Discord account unlinked!'), 'success') + return redirect(url_for('profil.profile')) \ No newline at end of file diff --git a/routes/example oauth.py b/routes/example oauth.py new file mode 100644 index 0000000..5e77fcf --- /dev/null +++ b/routes/example oauth.py @@ -0,0 +1,12 @@ +from authlib.integrations.flask_client import OAuth + +oauth = OAuth() +discord = oauth.register( + name='discord', + client_id='YOUR_CLIENT_ID', + client_secret='YOUR_CLIENT_SECRET', + access_token_url='https://discord.com/api/oauth2/token', + authorize_url='https://discord.com/api/oauth2/authorize', + api_base_url='https://discord.com/api/', + client_kwargs={'scope': 'identify email'} +) \ No newline at end of file diff --git a/routes/friends.py b/routes/friends.py new file mode 100644 index 0000000..ffb6d2b --- /dev/null +++ b/routes/friends.py @@ -0,0 +1,92 @@ +from flask import Blueprint, redirect, url_for, flash, render_template +from flask_login import login_required, current_user +from models import db, Notification, Event, User, Friendship, Reward +from flask_babel import gettext as _ + +friends_bp = Blueprint('friend', __name__) + +@friends_bp.route('/add_friend/', methods=['POST']) +@login_required +def add_friend(user_id): + if user_id == current_user.id: + flash(_('You cannot add yourself as a friend.'), 'warning') + return redirect(url_for('user.users')) + existing = db.session.query(Friendship).filter_by(requester_id=current_user.id, receiver_id=user_id).first() + if existing: + flash(_('Friend request already sent.'), 'info') + else: + friendship = Friendship(requester_id=current_user.id, receiver_id=user_id) + db.session.add(friendship) + db.session.commit() + friend = db.session.get(User, user_id) + event = Event(message=_(f"{current_user.username} sent a friend request to {friend.username}.")) + db.session.add(event) + db.session.commit() + notif = Notification( + user_id=user_id, + message=_(f"You have received a friend request from {current_user.username}.") + ) + db.session.add(notif) + db.session.commit() + flash(_('Friend request sent!'), 'success') + return redirect(url_for('user.users')) + +@friends_bp.route('/accept_friend/', methods=['POST']) +@login_required +def accept_friend(friendship_id): + friendship = db.session.get(Friendship, friendship_id) + if friendship and friendship.receiver_id == current_user.id: + friendship.status = 'accepted' + db.session.commit() + friend = db.session.get(User, friendship.requester_id) + event = Event(message=_(f"{current_user.username} und {friend.username} sind jetzt Freunde.")) + db.session.add(event) + db.session.add(Reward(user_id=current_user.id, type='friendship', points=5)) + db.session.add(Reward(user_id=friendship.requester_id, type='friendship', points=5)) + db.session.commit() + flash(_('Friend request accepted!'), 'success') + else: + flash(_('Invalid friend request.'), 'danger') + return redirect(url_for('friend.friends')) + +@friends_bp.route('/reject_friend/', methods=['POST']) +@login_required +def reject_friend(friendship_id): + friendship = db.session.get(Friendship, friendship_id) + if friendship and friendship.receiver_id == current_user.id: + friendship.status = 'rejected' + db.session.commit() + friend = db.session.get(User, friendship.requester_id) + event = Event(message=_(f"{current_user.username} has rejected {friend.username}'s friend request.")) + db.session.add(event) + db.session.commit() + flash(_('Friend request rejected.'), 'info') + else: + flash(_('Invalid friend request.'), 'danger') + return redirect(url_for('friend.friends')) + +@friends_bp.route('/friends') +@login_required +def friends(): + friends = db.session.query(User).join(Friendship, ((Friendship.requester_id == User.id) | (Friendship.receiver_id == User.id)))\ + .filter( + ((Friendship.requester_id == current_user.id) | (Friendship.receiver_id == current_user.id)), + Friendship.status == 'accepted', + User.id != current_user.id + ).all() + requests = db.session.query(Friendship).filter_by(receiver_id=current_user.id, status='pending').all() + return render_template('friends.html', friends=friends, requests=requests) + +@friends_bp.route('/remove_friend/', methods=['POST']) +@login_required +def remove_friend(user_id): + friendship = db.session.query(Friendship).filter( + ((Friendship.requester_id == current_user.id) & (Friendship.receiver_id == user_id)) | + ((Friendship.requester_id == user_id) & (Friendship.receiver_id == current_user.id)), + Friendship.status == 'accepted' + ).first() + if friendship: + db.session.delete(friendship) + db.session.commit() + flash(_('Friendship ended.'), 'info') + return redirect(url_for('friend.friends')) \ No newline at end of file diff --git a/routes/like.py b/routes/like.py new file mode 100644 index 0000000..aaa790e --- /dev/null +++ b/routes/like.py @@ -0,0 +1,41 @@ +from flask import Blueprint, redirect, url_for, flash +from flask_login import login_required, current_user +from models import db, Post, Notification, Like +from flask_babel import gettext as _ + +like_bp = Blueprint('like', __name__) + +@like_bp.route('/like/', methods=['POST', 'GET']) +@login_required +def like_post(post_id): + post = db.session.get(Post, post_id) + if not post: + flash(_('Post does not exist.'), 'danger') + return redirect(url_for('post.feed')) + like = db.session.query(Like).filter_by(post_id=post_id, user_id=current_user.id).first() + if not like: + db.session.add(Like(post_id=post_id, user_id=current_user.id)) + db.session.commit() + if post.user_id != current_user.id: + notif = Notification( + user_id=post.user_id, + message=_(f"{current_user.username} liked your post.") + ) + db.session.add(notif) + db.session.commit() + flash(_('Post liked.'), 'info') + return redirect(url_for('post.feed')) + +@like_bp.route('/unlike/', methods=['POST', 'GET']) +@login_required +def unlike_post(post_id): + post = db.session.get(Post, post_id) + if not post: + flash(_('Post does not exist.'), 'danger') + return redirect(url_for('post.feed')) + like = db.session.query(Like).filter_by(post_id=post_id, user_id=current_user.id).first() + if like: + db.session.delete(like) + db.session.commit() + flash(_('Like removed.'), 'info') + return redirect(url_for('post.feed')) \ No newline at end of file diff --git a/routes/login.py b/routes/login.py new file mode 100644 index 0000000..adcd4d9 --- /dev/null +++ b/routes/login.py @@ -0,0 +1,80 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request +from models import db, User, PasswordResetRequest +from flask_babel import gettext as _ +from flask_login import login_user, login_required, logout_user, current_user +from werkzeug.security import generate_password_hash, check_password_hash +import re + +log_bp = Blueprint('log', __name__) + +@log_bp.route('/login', methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + return redirect(url_for('post.feed')) + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + user = db.session.query(User).filter_by(username=username).first() + if user and check_password_hash(user.password, password): + login_user(user) + flash(_('Logged in successfully.'), 'success') + next_url = request.args.get('next') + if next_url: + return redirect(next_url) + return redirect(url_for('post.feed')) + else: + flash(_('Invalid username or password.'), 'danger') + return render_template('login.html') + +@log_bp.route('/logout') +@login_required +def logout(): + logout_user() + flash(_('Logged out successfully.'), 'success') + return redirect(url_for('index')) + +@log_bp.route('/register', methods=['GET', 'POST']) +def register(): + if current_user.is_authenticated: + return redirect(url_for('post.feed')) + if request.method == 'POST': + username = request.form['username'] + email = request.form['email'] + password = request.form['password'] + confirm_password = request.form['confirm_password'] + if password != confirm_password: + flash(_('Passwords do not match.'), 'danger') + elif db.session.query(User).filter_by(username=username).first(): + flash(_('Username already exists.'), 'danger') + elif db.session.query(User).filter_by(email=email).first(): + flash(_('E-Mail already exists.'), 'danger') + elif len(password) < 8: + flash(_('Password must be at least 8 characters long.'), 'danger') + elif not re.match(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', email): + flash(_('Invalid email address.'), 'danger') + elif not re.match(r'^[a-zA-Z0-9_.+-]+$', username): + flash(_('Invalid username. Only alphanumeric characters are allowed.'), 'danger') + elif len(username) < 3 or len(username) > 20: + flash(_('Username must be between 3 and 20 characters long.'), 'danger') + else: + hashed_password = generate_password_hash(password, method='pbkdf2:sha256') + new_user = User(username=username, email=email, password=hashed_password) + db.session.add(new_user) + db.session.commit() + flash(_('Registered successfully. You can now log in.'), 'success') + return redirect(url_for('log.login')) + return render_template('register.html') + +@log_bp.route('/reset_password', methods=['GET', 'POST']) +def reset_password(): + if request.method == 'POST': + username = request.form['username'] + user = db.session.query(User).filter_by(username=username).first() + if user: + req = PasswordResetRequest(user_id=user.id) + db.session.add(req) + db.session.commit() + flash(_('Reset request sent to admins.'), 'info') + else: + flash(_('No user with this email.'), 'danger') + return render_template('reset_password.html') \ No newline at end of file diff --git a/routes/notifications.py b/routes/notifications.py new file mode 100644 index 0000000..8ffa720 --- /dev/null +++ b/routes/notifications.py @@ -0,0 +1,33 @@ +from flask import Blueprint, redirect, url_for, flash, render_template +from flask_login import login_required, current_user +from models import db, Notification +from flask_babel import gettext as _ +from datetime import datetime, timedelta + +noti_bp = Blueprint('notif', __name__) + +@noti_bp.route('/delete_all_notifications', methods=['POST']) +@login_required +def delete_all_notifications(): + db.session.query(Notification).filter_by(user_id=current_user.id).delete() + db.session.commit() + flash(_('All notifications have been deleted.'), 'success') + return redirect(url_for('notif.notifications')) + +@noti_bp.route('/delete_notification/', methods=['POST']) +@login_required +def delete_notification(notif_id): + notif = db.session.get(Notification, notif_id) + if notif and notif.user_id == current_user.id: + db.session.delete(notif) + db.session.commit() + return redirect(url_for('notif.notifications')) + +@noti_bp.route('/notifications') +@login_required +def notifications(): + expire_time = datetime.now() - timedelta(days=3) + db.session.query(Notification).filter(Notification.created_at < expire_time).delete() + db.session.commit() + notifications = db.session.query(Notification).filter_by(user_id=current_user.id).order_by(Notification.created_at.desc()).all() + return render_template('notifications.html', notifications=notifications) \ No newline at end of file diff --git a/routes/post.py b/routes/post.py new file mode 100644 index 0000000..60f1cd9 --- /dev/null +++ b/routes/post.py @@ -0,0 +1,225 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request +from flask_login import login_required, current_user +from models import db, Post, Reward, Friendship, Upload, Notification, Event, SHOPITEM_ID_EXTRA_UPLOAD, SHOPITEM_ID_EXTRA_TYPES, Like, Comment +from flask_babel import gettext as _ +from sqlalchemy import func +from datetime import datetime, date +import os + +post_bp = Blueprint('post', __name__) + +@post_bp.route('/post', methods=['POST']) +@login_required +def create_post(): + content = request.form['content'] + visibility = request.form.get('visibility', 'public') + file = request.files.get('file') + file2 = request.files.get('file2') + post = None + if content: + post = Post(user_id=current_user.id, content=content, visibility=visibility) + if not SHOPITEM_ID_EXTRA_TYPES in [usi.item_id for usi in current_user.shop_items]: + if len(content) > 250: + flash(_('Post content is too long. Please limit it to 250 characters.'), 'danger') + return redirect(url_for('post.feed')) + else: + if len(content) > 500: + flash(_('Post content is too long. Please limit it to 500 characters.'), 'danger') + return redirect(url_for('post.feed')) + db.session.add(post) + db.session.commit() + flash(_('Post created!'), 'success') + if file and file.filename: + ext = os.path.splitext(file.filename)[1] + filename = f"user_{current_user.id}_{int(datetime.now().timestamp())}{ext}" + filepath = os.path.join('static/uploads', filename) + file.save(filepath) + upload = Upload(user_id=current_user.id, post_id=post.id, filename=filename, filetype=file.content_type) + db.session.add(upload) + db.session.commit() + if file2 and file2.filename and SHOPITEM_ID_EXTRA_UPLOAD in [usi.item_id for usi in current_user.shop_items]: + ext = os.path.splitext(file2.filename)[1] + filename = f"user_{current_user.id}_{int(datetime.now().timestamp())}_extra{ext}" + filepath = os.path.join('static/uploads', filename) + file2.save(filepath) + upload2 = Upload(user_id=current_user.id, post_id=post.id, filename=filename, filetype=file2.content_type) + db.session.add(upload2) + db.session.commit() + event = Event(message=_(f"{current_user.username} has created a new post.")) + db.session.add(event) + db.session.commit() + notif = Notification(user_id=current_user.id, message=_("You have created a new post.")) + db.session.add(notif) + db.session.add(Reward(user_id=current_user.id, type='post', points=5)) + db.session.commit() + + return redirect(url_for('post.feed')) + +@post_bp.route('/edit_post/', methods=['GET']) +@login_required +def edit_post(post_id): + post = db.session.get(Post, post_id) + if not post: + flash(_('Post does not exist.'), 'danger') + return redirect(url_for('post.feed')) + if post.user_id != current_user.id: + flash(_('You do not have permission to edit this post.'), 'danger') + return redirect(url_for('post.feed')) + return render_template('edit_post.html', post=post) + +@post_bp.route('/update_post/', methods=['POST', 'GET']) +@login_required +def update_post(post_id): + post = db.session.get(Post, post_id) + if not post: + flash(_('Post does not exist.'), 'danger') + return redirect(url_for('post.feed')) + if post.user_id != current_user.id: + flash(_('You do not have permission to edit this post.'), 'danger') + return redirect(url_for('post.feed')) + content = request.form['content'] + visibility = request.form.get('visibility', 'public') + post.content = content + post.visibility = visibility + file = request.files.get('upload') + file2 = request.files.get('upload2') + if not SHOPITEM_ID_EXTRA_TYPES in [usi.item_id for usi in current_user.shop_items]: + if len(post.content) > 250: + flash(_('Post content is too long. Please limit it to 250 characters.'), 'danger') + return redirect(url_for('post.feed')) + else: + if len(post.content) > 500: + flash(_('Post content is too long. Please limit it to 500 characters.'), 'danger') + return redirect(url_for('post.feed')) + + if file: + for upload in post.uploads: + file_path = os.path.join('static/uploads', upload.filename) + try: + if os.path.exists(file_path): + os.remove(file_path) + except Exception: + pass + + db.session.delete(upload) + + ext = os.path.splitext(file.filename)[1] + filename = f"user_{current_user.id}_{int(datetime.now().timestamp())}{ext}" + filepath = os.path.join('static/uploads', filename) + file.save(filepath) + upload = Upload(user_id=current_user.id, post_id=post.id, filename=filename, filetype=file.content_type) + db.session.add(upload) + + if file2 and SHOPITEM_ID_EXTRA_UPLOAD in [usi.item_id for usi in current_user.shop_items]: + for upload in post.uploads: + if upload.filename.endswith('_extra' + os.path.splitext(file2.filename)[1]): + file_path = os.path.join('static/uploads', upload.filename) + try: + if os.path.exists(file_path): + os.remove(file_path) + except Exception: + pass + + db.session.delete(upload) + + ext = os.path.splitext(file2.filename)[1] + filename = f"user_{current_user.id}_{int(datetime.now().timestamp())}_extra{ext}" + filepath = os.path.join('static/uploads', filename) + file2.save(filepath) + upload2 = Upload(user_id=current_user.id, post_id=post.id, filename=filename, filetype=file2.content_type) + db.session.add(upload2) + + notif = Notification(user_id=current_user.id, message=_("Your post has been updated.")) + db.session.add(notif) + event = Event(message=f"{current_user.username} has updated post {post.id}.") + db.session.add(event) + db.session.commit() + flash(_('Post updated!'), 'success') + return redirect(url_for('post.feed')) + +@post_bp.route('/feed') +def feed(): + if current_user.is_authenticated: + today = date.today() + reward_today = db.session.query(Reward).filter( + Reward.user_id == current_user.id, + Reward.type == 'daily', + func.date(Reward.created_at) == today + ).first() + + if not reward_today: + db.session.add(Reward(user_id=current_user.id, type='daily', points=10)) + db.session.commit() + + if current_user.is_admin: + posts = db.session.query(Post).order_by(Post.created_at.desc()).all() + else: + friend_ids = [ + f.requester_id if f.requester_id != current_user.id else f.receiver_id + for f in db.session.query(Friendship).filter( + ((Friendship.requester_id == current_user.id) | (Friendship.receiver_id == current_user.id)), + Friendship.status == 'accepted' + ).all() + ] + posts = db.session.query(Post).filter( + (Post.visibility == 'public') | + ((Post.visibility == 'friends') & (Post.user_id.in_(friend_ids + [current_user.id]))) + ).order_by(Post.created_at.desc()).all() + else: + posts = db.session.query(Post).filter_by(visibility='public').order_by(Post.created_at.desc()).all() + events = db.session.query(Event).order_by(Event.timestamp.desc()).limit(20).all() + return render_template('feed.html', posts=posts, events=events) + +@post_bp.route('/delete_post/', methods=['POST']) +@login_required +def delete_post(post_id): + post = db.session.get(Post, post_id) + if post and post.user_id == current_user.id: + for upload in post.uploads: + file_path = os.path.join('static/uploads', upload.filename) + try: + if os.path.exists(file_path): + os.remove(file_path) + except Exception: + pass + likes = db.session.query(Like).filter_by(post_id=post_id).all() + for like in likes: + db.session.delete(like) + db.session.delete(post) + comments = db.session.query(Comment).filter_by(post_id=post_id).all() + for comment in comments: + db.session.delete(comment) + notif = Notification(user_id=current_user.id, message=_("Your post has been deleted.")) + db.session.add(notif) + event = Event(message=_(f"{current_user.username} has deleted post {post.id}.")) + db.session.add(event) + db.session.commit() + flash(_('Post and all uploads deleted.'), 'success') + else: + flash(_('Not allowed.'), 'danger') + return redirect(url_for('post.feed')) + +@post_bp.route('/delete_comment/', methods=['POST']) +@login_required +def delete_comment(comment_id): + comment = db.session.get(Comment, comment_id) + if comment and comment.user_id == current_user.id: + db.session.delete(comment) + db.session.commit() + flash(_('Comment deleted.'), 'success') + else: + flash(_('Not allowed.'), 'danger') + return redirect(url_for('post.feed')) + +@post_bp.route('/comment/', methods=['POST']) +@login_required +def comment_post(post_id): + content = request.form['comment'] + if content: + comment = Comment(post_id=post_id, user_id=current_user.id, content=content) + db.session.add(comment) + notif = Notification(user_id=current_user.id, message=_("You have written a comment.")) + db.session.add(notif) + db.session.add(Reward(user_id=current_user.id, type='comment', points=2)) + db.session.commit() + return redirect(url_for('post.feed')) \ No newline at end of file diff --git a/routes/profile.py b/routes/profile.py new file mode 100644 index 0000000..1c8ecd3 --- /dev/null +++ b/routes/profile.py @@ -0,0 +1,66 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request +from flask_login import login_required, current_user +from models import db, User, Post +from flask_babel import gettext as _ +from werkzeug.security import generate_password_hash +import re + +profile_bp = Blueprint('profil', __name__) + +@profile_bp.route('/profile') +@login_required +def profile(): + return render_template('profile.html', user=current_user) + +@profile_bp.route('/my_posts') +@login_required +def my_posts(): + posts = db.session.query(Post).filter_by(user_id=current_user.id).order_by(Post.created_at.desc()).all() + return render_template('my_posts.html', posts=posts) + +@profile_bp.route('/edit_profile', methods=['GET', 'POST']) +@login_required +def edit_profile(): + if request.method == 'POST': + new_username = request.form['username'] + new_email = request.form['email'] + new_password = request.form['password'] + confirm_password = request.form['confirm_password'] + if not current_user.username or not current_user.email: + flash(_('Username and email cannot be empty.'), 'danger') + return redirect(url_for('profile.edit_profile')) + else: + if new_username and new_username != current_user.username: + if db.session.query(User).filter_by(username=new_username).first(): + flash(_('Username already taken.'), 'danger') + return redirect(url_for('profile.edit_profile')) + elif not re.match(r'^[a-zA-Z0-9_.+-]+$', new_username): + flash(_('Invalid username. Only alphanumeric characters are allowed.'), 'danger') + return redirect(url_for('profile.edit_profile')) + else: + current_user.username = new_username + elif new_email and new_email != current_user.email: + if not re.match(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', new_email): + flash(_('Invalid email address.'), 'danger') + return redirect(url_for('profile.edit_profile')) + elif db.session.query(User).filter_by(email=new_email).first(): + flash(_('E-Mail already taken.'), 'danger') + return redirect(url_for('profile.edit_profile')) + else: + current_user.email = new_email + elif new_password: + if len(new_password) < 8: + flash(_('Password must be at least 8 characters long.'), 'danger') + return redirect(url_for('profile.edit_profile')) + elif new_password != confirm_password: + flash(_('Passwords do not match.'), 'danger') + return redirect(url_for('profile.edit_profile')) + else: + current_user.password = generate_password_hash(new_password, method='pbkdf2:sha256') + else: + flash(_('No changes made.'), 'info') + return redirect(url_for('profil.profile')) + db.session.commit() + flash(_('Profile updated.'), 'success') + return redirect(url_for('profil.profile')) + return render_template('edit_profile.html', user=current_user) \ No newline at end of file diff --git a/routes/support.py b/routes/support.py new file mode 100644 index 0000000..9536fce --- /dev/null +++ b/routes/support.py @@ -0,0 +1,77 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request, abort +from models import db, SupportComment, SupportRequest +from flask_babel import gettext as _ +from flask_login import login_required, current_user +from datetime import datetime + +support_bp = Blueprint('support', __name__, url_prefix="/support") + +@support_bp.route('/', methods=['GET', 'POST']) +@login_required +def support(): + if request.method == 'POST': + title = request.form.get('title') + description = request.form.get('description') + if description and title: + ticket = SupportRequest( + user_id=current_user.id, + title=title, + status='open', + created_at=datetime.now() + ) + db.session.add(ticket) + db.session.commit() + db.session.add(SupportComment(request_id=ticket.id, user_id=current_user.id, message=description, created_at=datetime.now())) + db.session.commit() + flash(_('Support request created!'), 'success') + else: + flash(_('Title and message required!'), 'danger') + + if current_user.is_admin: + support_requests = db.session.query(SupportRequest).order_by(SupportRequest.created_at.desc()).all() + else: + support_requests = db.session.query(SupportRequest).filter_by(user_id=current_user.id).order_by(SupportRequest.created_at.desc()).all() + return render_template('support.html', support_requests=support_requests) + +@support_bp.route('/close/', methods=['POST']) +@login_required +def support_close(request_id): + ticket = db.session.get(SupportRequest, request_id) + if not ticket or (not current_user.is_admin and ticket.user_id != current_user.id): + abort(403) + ticket.status = 'closed' + db.session.commit() + flash(_('Ticket closed.'), 'success') + return redirect(url_for('support.support_thread', request_id=request_id)) + +@support_bp.route('/thread/', methods=['GET', 'POST']) +@login_required +def support_thread(request_id): + ticket = db.session.get(SupportRequest, request_id) + if not ticket or (not current_user.is_admin and ticket.user_id != current_user.id): + abort(403) + if request.method == 'POST' and ticket.status == 'open': + message = request.form.get('message') + if message: + db.session.add(SupportComment(request_id=request_id, user_id=current_user.id, message=message, created_at=datetime.now())) + db.session.commit() + flash(_('Comment added.'), 'success') + else: + flash(_('Message required!'), 'danger') + comments = db.session.query(SupportComment).filter_by(request_id=request_id).order_by(SupportComment.created_at.asc()).all() + return render_template('support_thread.html', ticket=ticket, comments=comments) + +@support_bp.route('/delete/', methods=['POST']) +@login_required +def support_delete(request_id): + if not current_user.is_admin: + abort(403) + ticket = db.session.get(SupportRequest, request_id) + if ticket: + db.session.query(SupportComment).filter_by(request_id=request_id).delete() + db.session.delete(ticket) + db.session.commit() + flash(_('Support ticket deleted.'), 'success') + else: + flash(_('Ticket not found.'), 'danger') + return redirect(url_for('support.support')) \ No newline at end of file diff --git a/routes/user.py b/routes/user.py new file mode 100644 index 0000000..5328501 --- /dev/null +++ b/routes/user.py @@ -0,0 +1,104 @@ +from flask import Blueprint, redirect, url_for, flash, request, render_template +from flask_login import logout_user +from flask_login import login_required, current_user +from models import db, Notification, Upload, Event, User +from flask_babel import gettext as _ +import os +from datetime import datetime + +__mapper_args__ = {"confirm_deleted_rows": False} +user_bp = Blueprint('user', __name__) + +@user_bp.route('/delete_pic', methods=['POST']) +@login_required +def delete_pic(): + if current_user.profile_pic and current_user.profile_pic != 'default.png': + try: + os.remove(os.path.join('static/profile_pics', current_user.profile_pic)) + except Exception: + pass + current_user.profile_pic = 'default.png' + db.session.commit() + flash(_('Profile picture deleted.'), 'success') + return redirect(url_for('profil.profile')) + +@user_bp.route('/upload_pic', methods=['POST']) +@login_required +def upload_pic(): + file = request.files['profile_pic'] + if file: + if current_user.profile_pic and current_user.profile_pic != 'default.png': + try: + os.remove(os.path.join('static/profile_pics', current_user.profile_pic)) + except Exception: + pass + current_user.profile_pic = 'default.png' + db.session.commit() + + ext = os.path.splitext(file.filename)[1] + filename = f"user_{current_user.id}_{int(datetime.now().timestamp())}{ext}" + filepath = os.path.join('static/profile_pics', filename) + file.save(filepath) + current_user.profile_pic = filename + db.session.commit() + + notif = Notification(user_id=current_user.id, message=_("You have changed your profile picture.")) + db.session.add(notif) + db.session.commit() + + event = Event(message=_(f"{current_user.username} has changed their profile picture.")) + db.session.add(event) + db.session.commit() + flash(_('Profile picture updated.'), 'success') + return redirect(url_for('profil.profile')) + +@user_bp.route('/delete_account', methods=['POST']) +@login_required +def delete_account(): + if current_user.is_owner: + flash(_('You cannot delete the owner account.'), 'danger') + return redirect(url_for('profil.profile')) + if current_user.is_admin: + flash(_('You cannot delete an admin account.'), 'danger') + return redirect(url_for('profil.profile')) + event = Event(message=f"{current_user.username} hat sein Konto gelöscht.") + db.session.add(event) + for post in current_user.posts: + db.session.delete(post) + for friendship in current_user.friendships_sent + current_user.friendships_received: + db.session.delete(friendship) + for comment in current_user.comments: + db.session.delete(comment) + for like in current_user.likes: + db.session.delete(like) + for shop_item in current_user.shop_items: + db.session.delete(shop_item) + for reward in current_user.rewards: + db.session.delete(reward) + for upload in current_user.uploads: + file_path = os.path.join('static/uploads', upload.filename) + try: + if os.path.exists(file_path): + os.remove(file_path) + except Exception: + pass + db.session.delete(upload) + if current_user.profile_pic and not current_user.profile_pic == 'default.png': + try: + os.remove(os.path.join('static/profile_pics', current_user.profile_pic)) + except Exception: + pass + notifications = db.session.query(Notification).filter_by(user_id=current_user.id).all() + for notif in notifications: + db.session.delete(notif) + db.session.delete(current_user) + db.session.commit() + logout_user() + flash(_('Account and all your data deleted.'), 'success') + return redirect(url_for('index')) + +@user_bp.route('/users') +@login_required +def users(): + all_users = db.session.query(User).filter(User.id != current_user.id).all() + return render_template('users.html', users=all_users) \ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..9dc4872 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,312 @@ +body { + background: linear-gradient(135deg, #e0e7ff 0%, #f0f2f5 100%); + min-height: 100vh; + font-family: 'Segoe UI', 'Roboto', Arial, sans-serif; + cursor: url("/static/icons/custom-cursor.png"), auto; +} + +canvas { + background: black; + border: 1px solid white; +} + +.navbar { + margin-bottom: 30px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + background: #fff; +} +.navbar-brand { + font-weight: bold; + color: #2563eb !important; + letter-spacing: 1px; +} +.profile-pic { + width: 56px; + height: 56px; + object-fit: cover; + border-radius: 50%; + border: 2px solid #2563eb; + background: #fff; +} +.card { + border-radius: 16px; + box-shadow: 0 2px 12px rgba(37,99,235,0.06); + border: none; +} +.card-body { + padding: 1.5rem; +} + +.btn-link { + text-decoration: none; +} + +.form-text { + margin-top: 0.25rem; + margin-bottom: 0.5rem; + display: block; +} + +.btn-primary, .btn-success, .btn-danger, .btn-warning { + border-radius: 20px; + padding-left: 1.2em; + padding-right: 1.2em; +} +.btn-primary { + background: linear-gradient(90deg, #2563eb 60%, #60a5fa 100%); + border: none; +} +.btn-primary:hover, .btn-primary:focus { + background: linear-gradient(90deg, #1d4ed8 60%, #3b82f6 100%); +} +.btn-danger { + background: linear-gradient(90deg, #ef4444 60%, #f87171 100%); + border: none; +} +.btn-danger:hover, .btn-danger:focus { + background: linear-gradient(90deg, #dc2626 60%, #f87171 100%); +} +.btn-success { + background: linear-gradient(90deg, #22c55e 60%, #4ade80 100%); + border: none; +} +.btn-success:hover, .btn-success:focus { + background: linear-gradient(90deg, #16a34a 60%, #4ade80 100%); +} +.btn-warning { + background: linear-gradient(90deg, #f59e42 60%, #fbbf24 100%); + border: none; + color: #fff; +} +.btn-warning:hover, .btn-warning:focus { + background: linear-gradient(90deg, #d97706 60%, #fbbf24 100%); + color: #fff; +} +.form-control, .form-select { + border-radius: 12px; + border: 1px solid #cbd5e1; + background: #f8fafc; +} +.form-control:focus { + border-color: #2563eb; + box-shadow: 0 0 0 2px #2563eb22; +} +.list-group-item { + border: none; + border-radius: 12px !important; + margin-bottom: 8px; + background: #fff; + box-shadow: 0 1px 4px rgba(37,99,235,0.04); +} +.alert { + border-radius: 12px; + font-size: 1rem; +} +.table { + background: #fff; + border-radius: 12px; + overflow: hidden; +} +.table th, .table td { + vertical-align: middle; +} + +::-webkit-scrollbar { + width: 8px; + background: #e0e7ff; +} +::-webkit-scrollbar-thumb { + background: #c7d2fe; + border-radius: 8px; +} + +/* Light Mode (default) */ +body, .card, .navbar, .list-group-item, .table, .form-control, .form-select { + transition: background 0.3s, color 0.3s; +} +body.light-mode { + background: linear-gradient(135deg, #0f5b86 0%, #0245aabb 100%); + color: #222; +} +body.light-mode .card, +body.light-mode .navbar, +body.light-mode .list-group-item, +body.light-mode .table { + background: #fff; + color: #222; +} +body.light-mode .form-control, +body.light-mode .form-select { + background: #f8fafc; + color: #222; +} + +body.light-mode li button { + color: #0011ff !important; +} + +/* Dark Mode */ +body.dark-mode { + background: linear-gradient(135deg, #0a0a52 0%, #000000 100%) !important; + color: #e5e7eb !important; +} + +body.dark-mode li button { + color: #0099f1 !important; +} + +body.light-mode p { + color: #000000 !important; +} + +body.dark-mode .card, +body.dark-mode .navbar, +body.dark-mode .list-group-item, +body.dark-mode .table { + background: #23272f !important; + color: #e5e7eb !important; + border-color: #23272f !important; +} +body.dark-mode .form-control, +body.dark-mode .form-select { + background: #23272f !important; + color: #e5e7eb !important; + border-color: #444 !important; +} +body.dark-mode .form-control:focus, +body.dark-mode .form-select:focus { + border-color: #2563eb !important; + box-shadow: 0 0 0 2px #2563eb55 !important; + background: #23272f !important; + color: #e5e7eb !important; +} +body.dark-mode .btn, +body.dark-mode .btn-primary, +body.dark-mode .btn-success, +body.dark-mode .btn-danger, +body.dark-mode .btn-warning { + filter: none !important; + background: #2563eb !important; + color: #fff !important; + border: none !important; +} +body.dark-mode .btn-danger { + background: #ef4444 !important; +} +body.dark-mode .btn-success { + background: #22c55e !important; +} +body.dark-mode .btn-warning { + background: #f59e42 !important; + color: #fff !important; +} +body.dark-mode .btn:hover, +body.dark-mode .btn:focus { + opacity: 0.9; +} +body.dark-mode .text-muted { + color: #9ca3af !important; +} +body.dark-mode small, +body.dark-mode p { + color: #e5e7eb !important; +} +body.dark-mode .alert { + background: #23272f !important; + color: #e5e7eb !important; + border-color: #444 !important; +} +body.dark-mode .navbar { + box-shadow: 0 2px 8px rgba(0,0,0,0.4) !important; +} +body.dark-mode .profile-pic { + border-color: #60a5fa; + background: #181a1b !important; +} +body.dark-mode .table th, +body.dark-mode .table td { + color: #e5e7eb !important; + background: #23272f !important; +} +body.dark-mode .list-group-item { + background: #23272f !important; + color: #e5e7eb !important; + box-shadow: 0 1px 4px rgba(37,99,235,0.08) !important; +} +body.dark-mode ::-webkit-scrollbar-thumb { + background: #374151 !important; +} +body.dark-mode ::-webkit-scrollbar { + background: #23272f !important; +} + +/* Links im Dark Mode */ +body.dark-mode a, +body.dark-mode a:visited { + color: #60a5fa !important; + text-decoration: none; +} +body.dark-mode a:hover { + color: #93c5fd !important; +} + +/* Placeholder (Input-Hint) im Dark Mode */ +body.dark-mode ::placeholder { + color: #b0b8c1 !important; + opacity: 1; +} +body.dark-mode :-ms-input-placeholder { /* IE 10+ */ + color: #b0b8c1 !important; +} +body.dark-mode ::-ms-input-placeholder { /* Edge */ + color: #b0b8c1 !important; +} +body.dark-mode .navbar-toggler-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255,255,255,0.9)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); +} + +/* Footer Styling */ +.footer { + background: #f8fafc; + border-top: 1px solid #e5e7eb; +} + +body.dark-mode .footer { + background: #181a1b !important; + border-top: 1px solid #23272f !important; +} + +.footer .text-muted, .footer a { + color: #6c757d !important; +} + +body.dark-mode .footer .text-muted, body.dark-mode .footer a { + color: #b0b8c1 !important; +} + +@media (max-width: 576px) { + .profile-pic { + width: 40px; + height: 40px; + } + .card-body { + padding: 1rem; + } + .navbar-brand { + font-size: 1.1rem; + } + .container { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + .card { + margin-bottom: 1rem; + } + .form-control, .form-select { + font-size: 1rem; + } + .btn { + font-size: 1rem; + padding: 0.5em 1em; + } +} \ No newline at end of file diff --git a/static/icons/custom-cursor.png b/static/icons/custom-cursor.png new file mode 100644 index 0000000..3172182 Binary files /dev/null and b/static/icons/custom-cursor.png differ diff --git a/static/icons/favicon.ico b/static/icons/favicon.ico new file mode 100644 index 0000000..e514548 Binary files /dev/null and b/static/icons/favicon.ico differ diff --git a/static/icons/icon-192.png b/static/icons/icon-192.png new file mode 100644 index 0000000..a6156eb Binary files /dev/null and b/static/icons/icon-192.png differ diff --git a/static/icons/icon-512.png b/static/icons/icon-512.png new file mode 100644 index 0000000..6e54dd9 Binary files /dev/null and b/static/icons/icon-512.png differ diff --git a/static/js/adstop.js b/static/js/adstop.js new file mode 100644 index 0000000..b00637f --- /dev/null +++ b/static/js/adstop.js @@ -0,0 +1,20 @@ +document.addEventListener("DOMContentLoaded", function () { + var hash = window.location.hash; + if (hash) { + var tabTrigger = document.querySelector('#adminTab button[data-bs-target="' + hash + '"]'); + if (tabTrigger) { + var tab = new bootstrap.Tab(tabTrigger); + tab.show(); + } + } + + var triggerTabList = [].slice.call(document.querySelectorAll('#adminTab button')); + triggerTabList.forEach(function (triggerEl) { + triggerEl.addEventListener('shown.bs.tab', function (event) { + var target = triggerEl.getAttribute('data-bs-target'); + if (target) { + history.replaceState(null, null, target); + } + }); + }); + }); \ No newline at end of file diff --git a/static/js/events.js b/static/js/events.js new file mode 100644 index 0000000..159969c --- /dev/null +++ b/static/js/events.js @@ -0,0 +1,16 @@ +function reloadEvents() { + fetch(apiEventsUrl) + .then(r => r.json()) + .then(events => { + let tbody = document.getElementById('events-tbody'); + tbody.innerHTML = ""; + for (let e of events) { + let tr = document.createElement('tr'); + tr.innerHTML = `${e.timestamp}${e.message}`; + tbody.appendChild(tr); + } + }); +} + +setInterval(reloadEvents, 10000); +window.onload = reloadEvents; \ No newline at end of file diff --git a/static/js/feed.js b/static/js/feed.js new file mode 100644 index 0000000..9a2d8d3 --- /dev/null +++ b/static/js/feed.js @@ -0,0 +1,5 @@ +function reload_feed() { + setTimeout(function() { + window.location.reload(); + }, 120000); +} \ No newline at end of file diff --git a/static/js/sw.js b/static/js/sw.js new file mode 100644 index 0000000..e929f83 --- /dev/null +++ b/static/js/sw.js @@ -0,0 +1,19 @@ +const CACHE_NAME = "minifb-v1"; +const urlsToCache = [ + "/", + "/static/css/styles.css", + "/static/js/theme.js", + "/static/manifest.json" +]; + +self.addEventListener("install", event => { + event.waitUntil( + caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache)) + ); +}); + +self.addEventListener("fetch", event => { + event.respondWith( + caches.match(event.request).then(response => response || fetch(event.request)) + ); +}); \ No newline at end of file diff --git a/static/js/theme.js b/static/js/theme.js new file mode 100644 index 0000000..1a5d921 --- /dev/null +++ b/static/js/theme.js @@ -0,0 +1,25 @@ +function setTheme(mode, save=true) { + document.body.classList.remove('light-mode', 'dark-mode'); + document.body.classList.add(mode + '-mode'); + document.getElementById('theme-icon').className = mode === 'dark' ? 'bi bi-moon-fill' : 'bi bi-sun-fill'; + document.getElementById('theme-label').textContent = mode === 'dark' ? 'Dark-Mode' : 'Light-Mode'; + if(save) document.cookie = "theme=" + mode + ";path=/;max-age=31536000"; +} +function getCookie(name) { + let v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)'); + return v ? v[2] : null; +} + +function systemPrefersDark() { + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; +} + +document.addEventListener('DOMContentLoaded', function() { + let theme = getCookie('theme'); + if(!theme) theme = systemPrefersDark() ? 'dark' : 'light'; + setTheme(theme, false); + document.getElementById('toggle-theme').onclick = function() { + let newTheme = document.body.classList.contains('dark-mode') ? 'light' : 'dark'; + setTheme(newTheme); + }; +}); \ No newline at end of file diff --git a/static/js/translate.js b/static/js/translate.js new file mode 100644 index 0000000..d01206e --- /dev/null +++ b/static/js/translate.js @@ -0,0 +1,9 @@ +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.lang-select').forEach(function(el) { + el.addEventListener('click', function(e) { + e.preventDefault(); + document.cookie = "lang=" + this.dataset.lang + ";path=/"; + location.reload(); + }); + }); +}); \ No newline at end of file diff --git a/static/manifest.json b/static/manifest.json new file mode 100644 index 0000000..55ea41f --- /dev/null +++ b/static/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "MiniFacebook", + "short_name": "MiniFB", + "start_url": "/", + "display": "standalone", + "background_color": "#181a1b", + "theme_color": "#2563eb", + "description": "MiniFacebook Social App", + "icons": [ + { + "src": "/static/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/static/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/templates/403.html b/templates/403.html new file mode 100644 index 0000000..5b6a167 --- /dev/null +++ b/templates/403.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block title %}{{ _('Home') }}{% endblock %} +{% block content %} +
+
+
+
+

+ MiniFacebook 403 +

+

+ {{ _('This page is not accessible to you, you do not have the permissions to view it.') }}
+ {{ _('You can go back from the page.') }} +

+ {{ _('Go to Feed') }} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..4820f9f --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,372 @@ +{% extends "base.html" %} +{% block title %}{{ _('Admin') }}{% endblock %} +{% block content %} +

{{ _('Admin Panel') }}

+ + + +
+ +
+

{{ _('Users') }}

+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% endfor %} + +
{{ _('User') }}{{ _('Email') }}{{ _('Admin') }}{{ _('Owner') }}{{ _('Profile Pic') }}{{ _('Actions') }}
{% if user.profile_pic and user.profile_pic != 'default.png' %}{% endif %} {{ user.username }}{{ user.email }}{% if user.is_admin %}{% endif %}{% if user.is_owner %}{% endif %} + {% if user.profile_pic and user.profile_pic != 'default.png' %} + +
+ +
+ {% endif %} +
+ {% if not user.is_admin %} +
+ +
+ {% elif not user.is_owner %} +
+ +
+ {% endif %} + {% if not user.is_owner %} +
+ +
+ {% endif %} +
+
+ + +
+

{{ _('All Posts') }}

+ + + + + + + + + + + + {% for post in posts %} + + + + + + + + {% endfor %} + +
{{ _('User') }}{{ _('Content') }}{{ _('Visibility') }}{{ _('Created') }}{{ _('Actions') }}
+ {% if post.user.profile_pic and post.user.profile_pic != 'default.png' %} + {{ post.user.username }}{{ post.user.username }} + {% else %} + {{ post.user.username }} + {% endif %} + {{ post.content|truncate(50) }} + {% if post.visibility == 'public' %} + {{ _('Public') }} + {% else %} + {{ _('Friends') }} + {% endif %} + {{ post.created_at.strftime('%Y-%m-%d %H:%M') }} +
+ +
+
+
+ + +
+

{{ _('Friendships') }}

+ + + + + + + + + + {% for f in friendships %} + + + + + + {% endfor %} + +
{{ _('User 1') }}{{ _('User 2') }}{{ _('Status') }}
+ {% if f.requester.profile_pic and f.requester.profile_pic != 'default.png' %} + {{ f.requester.username }}{{ f.requester.username }} + {% else %} + {{ f.requester.username }} + {% endif %} + + {% if f.receiver.profile_pic and f.receiver.profile_pic != 'default.png' %} + {{ f.receiver.username }}{{ f.receiver.username }} + {% else %} + {{ f.receiver.username }} + {% endif %} + + {% if f.status == 'accepted' %} + {{ _('Accepted') }} + {% elif f.status == 'pending' %} + {{ _('Pending') }} + {% else %} + {{ _('Rejected') }} + {% endif %} +
+
+ + +
+

{{ _('Comments') }}

+ + + + + + + + + + + + {% for comment in comments %} + + + + + + + + {% endfor %} + +
{{ _('User') }}{{ _('Post') }}{{ _('Content') }}{{ _('Created') }}{{ _('Actions') }}
+ {% if comment.user.profile_pic and comment.user.profile_pic != 'default.png' %} + {{ comment.user.username }} + {% else %} + {{ comment.user.username }} + {% endif %} + {{ comment.post.id }}{{ comment.content|truncate(50) }}{{ comment.created_at.strftime('%Y-%m-%d %H:%M') }} +
+ +
+
+
+ + +
+

{{ _('Uploads') }} +
+ +
+

+ + + + + + + + + + + + {% for upload in uploads %} + + + + + + + + {% endfor %} + +
{{ _('User') }}{{ _('Filename') }}{{ _('Type') }}{{ _('Uploaded') }}{{ _('Actions') }}
{{ upload.user.username }}{{ upload.filename }}{{ upload.filetype }}{{ upload.uploaded_at.strftime('%Y-%m-%d %H:%M') }} + +
+ +
+
+
+ + +
+

+ {{ _('Notifications') }} +
+ +
+

+ + + + + + + + + + {% for notif in all_notifications %} + + + + + + {% endfor %} + +
{{ _('User') }}{{ _('Message') }}{{ _('Created') }}
{{ notif.user_id }}{{ notif.message }}{{ notif.created_at.strftime('%Y-%m-%d %H:%M') }}
+
+ + +
+

+ {{ _('Recent Events') }} +
+ +
+

+ + + {% for event in events %} + + + + + {% endfor %} + +
{{ event.timestamp.strftime('%Y-%m-%d %H:%M') }}{{ event.message }}
+
+ + +
+

{{ _('Shop Orders') }}

+ + + + + + + + + {% for usi in user_shop_items %} + + + + + {% endfor %} + +
{{ _('User') }}{{ _('Item') }}
+ {% if usi.user.profile_pic and usi.user.profile_pic != 'default.png' %} + {{ usi.user.username }} + {% else %} + {{ usi.user.username }} + {% endif %} + {{ usi.item.name }}
+
+ + +
+

{{ _('Reward Points') }}

+ + + + + + + + + + {% for user in users %} + + + + + + {% endfor %} + +
{{ _('User') }}{{ _('Points') }}{{ _('Actions') }}
+ {% if user.profile_pic and user.profile_pic != 'default.png' %} + {{ user.username }} + {% else %} + {{ user.username }} + {% endif %} + {{ user.reward_points() }} +
+ + + +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/admin_set_password.html b/templates/admin_set_password.html new file mode 100644 index 0000000..5f96dc7 --- /dev/null +++ b/templates/admin_set_password.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %}{{ _('Set New Password') }}{% endblock %} +{% block content %} +

{{ _('Set New Password for %(username)s', username=user.username) }}

+
+
+ + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..6b69160 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,113 @@ + + + + + + {% block title %}MiniFacebook{% endblock %} + + + + + + + + + + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} +
+ {% if user.profile_pic and user.profile_pic != 'default.png' %} + {% if SHOPITEM_ID_GOLDRAHMEN in user.shop_items | map(attribute='item_id') | list %} + + {% else %} + + {% endif %} + {% endif %} + {% if user.is_authenticated %} +

{{ _('Welcome, %(username)s!', username=user.username) }}

+ {% endif %} +
+ {% if user.is_admin %} +

{{ _('You are logged in as an admin.') }}

+ {% endif %} + {% if SHOPITEM_ID_PREMIUM in user.shop_items | map(attribute='item_id') | list %} + Premium + {% endif %} + {% block content %}{% endblock %} +
+ + + \ No newline at end of file diff --git a/templates/credits.html b/templates/credits.html new file mode 100644 index 0000000..0a11b26 --- /dev/null +++ b/templates/credits.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block title %}{{ _('Credits') }}{% endblock %} +{% block content %} +

{{ _('Credits') }}

+

{{ _('This project was developed by') }} Michatec. {{ _('Special thanks to all contributors and supporters.') }}

+

{{ _('Translators:') }}

+
    +
  • {{ _('German:') }} Michatec
  • +
  • {{ _('English:') }} Michatec
  • +
+

{{ _('Special thanks to the open-source community for their invaluable resources and tools.') }}

+

{{ _('Design inspired by various open-source projects and communities.') }}

+

{{ _('Backend powered by Flask and SQLAlchemy.') }}

+

{{ _('Frontend built with Bootstrap and jQuery.') }}

+

{{ _('Icons by FontAwesome and other open-source resources.') }}

+

{{ _('Hosted on a secure and reliable platform.') }}

+

{{ _('If you would like to contribute, please reach out to us.') }}

+{{ _('GitHub Repository') }} +

{{ _('Thank you for using our application!') }}

+{% endblock %} \ No newline at end of file diff --git a/templates/discord_register.html b/templates/discord_register.html new file mode 100644 index 0000000..0ca1987 --- /dev/null +++ b/templates/discord_register.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% block title %}{{ _('Discord Registration') }}{% endblock %} +{% block content %} +

{{ _('Complete Registration') }}

+

{{ _('Welcome,') }} {{ username }}!

+

{{ _('Please set a password for your account:') }}

+
+ + + +
+ + +
+
+ + +
+ + {{ _('Cancel') }} +
+{% endblock %} \ No newline at end of file diff --git a/templates/edit_post.html b/templates/edit_post.html new file mode 100644 index 0000000..49efe8c --- /dev/null +++ b/templates/edit_post.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% block title %}{{ _('Edit Post') }}{% endblock %} +{% block content %} +

{{ _('Edit Post') }}

+
+
+ + {% if not SHOPITEM_ID_EXTRA_TYPES in current_user.shop_items | map(attribute='item_id') | list %} +

{{ _('Limit: 250') }}

+ {% else %} +

{{ _('Limit: 500') }}

+ {% endif %} + +
+
+ + +
+
+ + + {% if SHOPITEM_ID_EXTRA_UPLOAD in current_user.shop_items | map(attribute='item_id') | list %} + + {% endif %} + {{ _('You can upload images, videos, audio files, or documents.') }} +
+ + {{ _('Cancel') }} +
+{% if post.uploads %} +
+

{{ _('Current Uploads') }}

+ {% for upload in post.uploads %} +
+ {% if upload.filetype.startswith('image') %} + + {% elif upload.filetype.startswith('video') %} + + {% elif upload.filetype.startswith('audio') %} + + {% else %} + {{ upload.filename }} + {% endif %} +
+ {% endfor %} +
+{% else %} +

{{ _('No uploads found for this post.') }}

+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/edit_profile.html b/templates/edit_profile.html new file mode 100644 index 0000000..b814c0b --- /dev/null +++ b/templates/edit_profile.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block title %}{{ _('Edit Profile') }}{% endblock %} +{% block content %} +

{{ _('Edit Profile') }}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/feed.html b/templates/feed.html new file mode 100644 index 0000000..e6b8fc1 --- /dev/null +++ b/templates/feed.html @@ -0,0 +1,122 @@ +{% extends "base.html" %} +{% block title %}{{ _('Feed') }}{% endblock %} +{% block content %} +{% if current_user.is_authenticated %} +
+ {% if not SHOPITEM_ID_EXTRA_TYPES in current_user.shop_items | map(attribute='item_id') | list %} +

{{ _('Limit: 250') }}

+ {% else %} +

{{ _('Limit: 500') }}

+ {% endif %} + + + {% if SHOPITEM_ID_EXTRA_UPLOAD in current_user.shop_items | map(attribute='item_id') | list %} + + {% endif %} + {{ _('You can upload images, videos, audio files, or documents.') }} + + +
+{% else %} + +{% endif %} +{% for post in posts %} +
+
+
+ {% if post.user.profile_pic and post.user.profile_pic != 'default.png' %} + {% if SHOPITEM_ID_GOLDRAHMEN in post.user.shop_items | map(attribute='item_id') | list %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {{ post.user.username }} +
+

{{ post.content }}

+ {% for upload in post.uploads %} + {% if upload.filetype.startswith('image') %} + + + {% elif upload.filetype.startswith('video') %} + + {{ _('Download Video') }} + {% elif upload.filetype.startswith('audio') %} + + {{ _('Download Audio') }} + {% else %} + {{ upload.filename }} + {% endif %} + {% endfor %} +
+ +
+
+ +
+ {% if post.user_id == current_user.id %} +
+ +
+
+ +
+ {% endif %} + {% if post.visibility == 'friends' %} + {{ _('Friends only') }} + {% else %} + {{ _('Public') }} + {% endif %} + {{ post.created_at.strftime('%Y-%m-%d %H:%M') }} +
+
+ +
+ {% if not current_user.is_authenticated %} + {{ _('Please login to comment.') }} + {% endif %} + {% for comment in post.comments %} +
+ {% if comment.user.profile_pic and comment.user.profile_pic != 'default.png' %} + {{ comment.user.username }}: + {% else %} + {{ comment.user.username }}: + {% endif %} + {{ comment.content }} + {% if comment.user_id == current_user.id %} +
+ +
+ {% endif %} +
+ {% endfor %} +
+
+
+{% endfor %} +{% if not posts %} + +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/templates/friends.html b/templates/friends.html new file mode 100644 index 0000000..ebe991b --- /dev/null +++ b/templates/friends.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} +{% block title %}{{ _('Friends') }}{% endblock %} +{% block content %} +

{{ _('Your Friends') }}

+
    + {% for friend in friends %} +
  • + + {% if friend.profile_pic and friend.profile_pic != 'default.png' %} + {{ friend.username }}{{ friend.username }} + {% else %} + {{ friend.username }} + {% endif %} + +
    + +
    +
  • + {% else %} +
  • {{ _('No friends yet.') }}
  • + {% endfor %} +
+

{{ _('Friend Requests') }}

+
    + {% for req in requests %} +
  • + + {% if req.requester.profile_pic and req.requester.profile_pic != 'default.png' %} + {{ req.requester.username }}{{ req.requester.username }} + {% else %} + {{ req.requester.username }} + {% endif %} + +
    +
    + +
    +
    + +
    +
    +
  • + {% else %} +
  • {{ _('No new requests') }}
  • + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..61c9942 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% block title %}{{ _('Home') }}{% endblock %} +{% block content %} +
+
+
+
+

+ MiniFacebook +

+

+ {{ _('MiniFacebook is a minimalist social network for sharing posts, images, and messages with friends.') }}
+ {{ _('Fast, simple, data-saving - and with') }} {{ _('Dark Mode!') }} +

+ {% if not current_user.is_authenticated %} + {{ _('Login') }} + {{ _('Register') }} + {{ _('Go to Feed') }} + {% else %} + {{ _('Go to Feed') }} + {% endif %} +
+
+
+
+

+ {{ _('About MiniFacebook') }} +

+
    +
  • {{ _('Share posts, images & videos') }}
  • +
  • {{ _('Friendships & notifications') }}
  • +
  • {{ _('Modern') }} {{ _('Dark/Light Mode') }}
  • +
  • {{ _('Open Source & privacy-friendly') }}
  • +
+
+

MiniFacebook {{ _('is a private, data-saving network for you and your friends. No ads, no data sharing - just fun in sharing and communicating.') }}

+

{{ _('Developed with love,') }} Bootstrap, {{ _('Dark Mode!') }}

+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..d3162f1 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% block title %}{{ _('Login') }}{% endblock %} +{% block content %} +
+
+
+

{{ _('Login') }}

+
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/my_posts.html b/templates/my_posts.html new file mode 100644 index 0000000..60aaa9d --- /dev/null +++ b/templates/my_posts.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} +{% block title %}{{ _('My Posts') }}{% endblock %} +{% block content %} +

{{ _('My Posts') }}

+{% for post in posts %} +
+
+
+ {% if post.user.profile_pic and post.user.profile_pic != 'default.png' %} + {% if SHOPITEM_ID_GOLDRAHMEN in post.user.shop_items | map(attribute='item_id') | list %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {{ post.user.username }} +
+

{{ post.content }}

+ {% for upload in post.uploads %} + {% if upload.filetype.startswith('image') %} + + + {% elif upload.filetype.startswith('video') %} + + {{ _('Download Video') }} + {% elif upload.filetype.startswith('audio') %} + + {{ _('Download Audio') }} + {% else %} + {{ upload.filename }} + {% endif %} + {% endfor %} +
+ +
+
+ +
+
+ +
+
+ +
+ {% if post.visibility == 'friends' %} + {{ _('Friends only') }} + {% else %} + {{ _('Public') }} + {% endif %} + {{ post.created_at.strftime('%Y-%m-%d %H:%M') }} +
+
+ +
+ {% for comment in post.comments %} +
+ {% if comment.user.profile_pic and comment.user.profile_pic != 'default.png' %} + {{ comment.user.username }}: + {% else %} + {{ comment.user.username }}: + {% endif %} + {% if comment.user_id == current_user.id %} +
+ +
+ {% endif %} +
+ {% endfor %} +
+
+
+{% endfor %} +{% if not posts %} + +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/notifications.html b/templates/notifications.html new file mode 100644 index 0000000..491b2b7 --- /dev/null +++ b/templates/notifications.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}{{ _('Notifications') }}{% endblock %} +{% block content %} +

+ + {{ _('Notifications') }} + {{ notifications|length }} + {% if notifications %} +
+ +
+ {% endif %} +

+
    + {% for notif in notifications %} +
  • + + + {{ notif.message }} + {{ notif.created_at.strftime('%Y-%m-%d %H:%M') }} + +
    + +
    +
  • + {% else %} +
  • {{ _('No notifications') }}
  • + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/privacy_policy.html b/templates/privacy_policy.html new file mode 100644 index 0000000..d802256 --- /dev/null +++ b/templates/privacy_policy.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block title %}{{ _('Privacy Policy') }}{% endblock %} +{% block content %} +

{{ _('Privacy Policy') }}

+

{{ _('Your privacy is important to us. This privacy policy explains how we collect, use, and protect your information when you use our application.') }}

+

{{ _('We collect personal information that you provide to us, such as your name, email address, and any other information you choose to share.') }}

+

{{ _('We use this information to provide and improve our services, communicate with you, and personalize your experience.') }}

+

{{ _('We do not share your personal information with third parties without your consent, except as required by law or to protect our rights.') }}

+

{{ _('We implement security measures to protect your information from unauthorized access, alteration, disclosure, or destruction.') }}

+

{{ _('You have the right to access, correct, or delete your personal information at any time. Please contact us if you wish to exercise these rights.') }}

+

{{ _('We may update this privacy policy from time to time. We will notify you of any changes by posting the new privacy policy on this page.') }}

+

{{ _('By using our application, you agree to the terms of this privacy policy. If you do not agree, please do not use our application.') }}

+

{{ _('If you have any questions or concerns about this privacy policy, please contact us.') }}

+{{ _('GitHub Repository') }} +

{{ _('Thank you for using our application!') }}

+{% endblock %} \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..98793b3 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}{{ _('Profile') }}{% endblock %} +{% block content %} +

{{ _('Email') }}: {{ user.email }}

+ +
+ + +
+{% if user.profile_pic and user.profile_pic != 'default.png' %} +
+ +
+{% endif %} + {{ _('Shop') }} + {{ _('Edit Profile') }} +
+ +
+{% if not user.discord_linked %} + + {{ _('Link Discord Account') }} + +{% else %} + {{ _('Discord Linked') }} +
+ +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..b33da65 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block title %}{{ _('Register') }}{% endblock %} +{% block content %} +
+
+
+

{{ _('Register') }}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ {{ _('Already have an account?') }} + {{ _('Login') }} +
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/reset_password.html b/templates/reset_password.html new file mode 100644 index 0000000..2194335 --- /dev/null +++ b/templates/reset_password.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block title %}{{ _('Reset Password') }}{% endblock %} +{% block content %} +
+
+
+

{{ _('Reset Password') }}

+
+
+ + +
+
+ +
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/reset_requests.html b/templates/reset_requests.html new file mode 100644 index 0000000..4383fc9 --- /dev/null +++ b/templates/reset_requests.html @@ -0,0 +1,90 @@ +{% extends "base.html" %} +{% block title %}{{ _('Password Reset Requests') }}{% endblock %} +{% block content %} +

+ {{ _('Password Reset Requests') }} + {{ _('Delete All') }} +

+ + + + + + + + + + {% for req in requests %} + + + + + + {% endfor %} + +
{{ _('User') }} {{ _('Requested At') }} {{ _('Actions') }}
+ {% if req.user.profile_pic and req.user.profile_pic != 'default.png' %} + {{ req.user.username }} + {% else %} + {{ req.user.username }} + {% endif %} + {{ req.requested_at.strftime('%Y-%m-%d %H:%M') }} + {{ _('Set Password') }} +
+ +
+
+{% if requests_done %} +

{{ _('Completed Requests') }}

+ + + + + + + + + {% for req in requests_done %} + + + + + {% endfor %} + +
{{ _('User') }} {{ _('Requested At') }}
+ {% if req.user.profile_pic and req.user.profile_pic != 'default.png' %} + {{ req.user.username }} + {% else %} + {{ req.user.username }} + {% endif %} + {{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}
+{% endif %} +{% if requests_rejected %} +

{{ _('Rejected Requests') }}

+ + + + + + + + + {% for req in requests_rejected %} + + + + + {% endfor %} + +
{{ _('User') }} {{ _('Requested At') }}
+ {% if req.user.profile_pic and req.user.profile_pic != 'default.png' %} + {{ req.user.username }} + {% else %} + {{ req.user.username }} + {% endif %} + {{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}
+{% endif %} +{% if not requests %} +
{{ _('No open reset requests.') }}
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/secret.html b/templates/secret.html new file mode 100644 index 0000000..fcdd5ae --- /dev/null +++ b/templates/secret.html @@ -0,0 +1,294 @@ +{% extends 'base.html' %} +{% block title %}{{ _('Secret') }}{% endblock %} +{% block content %} +

Secret

+ + +{% endblock %} \ No newline at end of file diff --git a/templates/setup.html b/templates/setup.html new file mode 100644 index 0000000..b587873 --- /dev/null +++ b/templates/setup.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block title %}{{ _('Admin Setup') }}{% endblock %} +{% block content %} +

{{ _('Admin Account Setup') }}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/shop.html b/templates/shop.html new file mode 100644 index 0000000..78c2d03 --- /dev/null +++ b/templates/shop.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% block title %}{{ _('Shop') }}{% endblock %} +{% block content %} +

{{ _('Shop') }}

+

{{ _('Deine Reward-Punkte:') }} {{ current_user.reward_points() }}

+{% if message %} +
{{ message }}
+{% endif %} +
+ {% for item in items %} +
+
+
+ +
{{ item.name }}
+

{{ item.description }}

+

{{ item.price }} {{ _('Points') }}

+ {% if item.id in owned_ids %} + + {% else %} +
+ + +
+ {% endif %} +
+
+
+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/support.html b/templates/support.html new file mode 100644 index 0000000..7fdc231 --- /dev/null +++ b/templates/support.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% block title %}{{ _('Support') }}{% endblock %} +{% block content %} +
+

{{ _('If you have any questions or need assistance, please contact us at:') }}

+

{{ _('Github:') }} https://github.com/Michatec/MiniFaceBook/issues

+
+{% if user.is_owner %} +
+ +
+{% endif %} +

{{ _('Support') }}

+
+ + + +
+ + + + + + + + + + + {% for ticket in support_requests %} + + + + + + + {% endfor %} + +
{{ _('Title') }}{{ _('Status') }}{{ _('Created') }}
{{ ticket.title }} + {% if ticket.status == 'open' %} + {{ _('Open') }} + {% else %} + {{ _('Closed') }} + {% endif %} + {{ ticket.created_at.strftime('%Y-%m-%d %H:%M') }} + {{ _('View') }} +
+{% endblock %} \ No newline at end of file diff --git a/templates/support_thread.html b/templates/support_thread.html new file mode 100644 index 0000000..0b151be --- /dev/null +++ b/templates/support_thread.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% block title %}{{ ticket.title }}{% endblock %} +{% block content %} +

{{ ticket.title }}

+

{{ _('Status') }}: + {% if ticket.status == 'open' %} + {{ _('Open') }} + {% else %} + {{ _('Closed') }} + {% endif %} +

+
+ {% for comment in comments %} +
+ {{ comment.user.username }} + {{ comment.created_at.strftime('%Y-%m-%d %H:%M') }} +

{{ comment.message }}

+
+ {% endfor %} +
+{% if ticket.status == 'open' %} +
+ + +
+
+ +
+{% else %} +
{{ _('This ticket is closed.') }}
+{% endif %} +{% if user.is_admin %} +
+ +
+{% endif %} +{{ _('Back to Support') }} +{% endblock %} \ No newline at end of file diff --git a/templates/users.html b/templates/users.html new file mode 100644 index 0000000..de6d2bf --- /dev/null +++ b/templates/users.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% block title %}{{ _('Users') }}{% endblock %} +{% block content %} +

{{ _('All Users') }}

+
    + {% for user_item in users %} +
  • + + {% if user_item.profile_pic and user_item.profile_pic != 'default.png' %} + {{ user_item.username }}{{ user_item.username }} + {% else %} + {{ user_item.username }} + {% endif %} + +
    + {% if user_item.id != user.id %} + {% set friendship = (user.friendships_sent | selectattr('receiver_id', 'equalto', user_item.id) | list) %} + {% set reverse_friendship = (user.friendships_received | selectattr('requester_id', 'equalto', user_item.id) | list) %} + {% if friendship and friendship[0].status == 'pending' %} + {{ _('Request sent') }} + {% elif reverse_friendship and reverse_friendship[0].status == 'pending' %} + {{ _('Request received') }} + {% elif (friendship and friendship[0].status == 'accepted') or (reverse_friendship and reverse_friendship[0].status == 'accepted') %} + {{ _('Friend') }} + {% else %} +
    + +
    + {% endif %} + {% endif %} + {% if user.is_admin %} + {% if not user_item.is_admin %} +
    + +
    + {% elif not user_item.is_owner %} +
    + +
    + {% endif %} + {% endif %} +
    +
  • + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/translations/de/LC_MESSAGES/messages.po b/translations/de/LC_MESSAGES/messages.po new file mode 100644 index 0000000..21256e8 --- /dev/null +++ b/translations/de/LC_MESSAGES/messages.po @@ -0,0 +1,1166 @@ +# German translations for PROJECT. +# Copyright (C) 2025 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2025-09-27 20:09+0200\n" +"PO-Revision-Date: 2025-09-27 20:09+0200\n" +"Last-Translator: FULL NAME \n" +"Language: de\n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.17.0\n" + +#: main.py:34 +msgid "Please log in to access this page." +msgstr "" + +#: main.py:117 routes/discord.py:44 routes/login.py:46 routes/profile.py:56 +msgid "Passwords do not match." +msgstr "" + +#: main.py:119 routes/login.py:48 +msgid "Username already exists." +msgstr "" + +#: main.py:121 routes/login.py:50 +msgid "E-Mail already exists." +msgstr "" + +#: main.py:123 routes/discord.py:41 routes/login.py:52 routes/profile.py:53 +msgid "Password must be at least 8 characters long." +msgstr "" + +#: main.py:125 routes/login.py:54 routes/profile.py:44 +msgid "Invalid email address." +msgstr "" + +#: main.py:127 routes/login.py:56 routes/profile.py:38 +msgid "Invalid username. Only alphanumeric characters are allowed." +msgstr "" + +#: main.py:133 +msgid "Admin account created. You can now log in." +msgstr "" + +#: main.py:159 +msgid "Already purchased!" +msgstr "" + +#: main.py:167 +msgid "Not enough points!" +msgstr "" + +#: routes/admin.py:18 +msgid "All password reset requests have been deleted." +msgstr "" + +#: routes/admin.py:65 +msgid "Request rejected." +msgstr "" + +#: routes/admin.py:79 +msgid "Password too short." +msgstr "" + +#: routes/admin.py:115 +msgid "Post and associated files deleted." +msgstr "" + +#: routes/admin.py:123 +msgid "Cannot delete the owner account." +msgstr "" + +#: routes/admin.py:158 +msgid "User deleted." +msgstr "" + +#: routes/admin.py:160 +msgid "Cannot delete admin or user not found." +msgstr "" + +#: routes/admin.py:186 routes/notifications.py:14 +msgid "All notifications have been deleted." +msgstr "" + +#: routes/admin.py:196 +msgid "All events have been deleted." +msgstr "" + +#: routes/admin.py:214 +msgid "Upload deleted." +msgstr "" + +#: routes/admin.py:232 +msgid "All uploads have been deleted." +msgstr "" + +#: routes/admin.py:244 +msgid "No Points entered!" +msgstr "" + +#: routes/admin.py:252 +msgid "Points added!" +msgstr "" + +#: routes/admin.py:258 +msgid "Points removed!" +msgstr "" + +#: routes/admin.py:260 +msgid "The user has not enough points to take!" +msgstr "" + +#: routes/admin.py:286 +msgid "Owner cannot be removed!" +msgstr "" + +#: routes/admin.py:329 +msgid "All Data has been deleted." +msgstr "" + +#: routes/discord.py:24 +msgid "Logged in with Discord." +msgstr "" + +#: routes/discord.py:27 +msgid "No account linked with this Discord. Please register." +msgstr "" + +#: routes/discord.py:47 +msgid "Username already exists. Please Report It." +msgstr "" + +#: routes/discord.py:60 +msgid "Account created and logged in with Discord." +msgstr "" + +#: routes/discord.py:77 +msgid "Discord account linked!" +msgstr "" + +#: routes/discord.py:86 +msgid "Discord account unlinked!" +msgstr "" + +#: routes/friends.py:12 +msgid "You cannot add yourself as a friend." +msgstr "" + +#: routes/friends.py:16 +msgid "Friend request already sent." +msgstr "" + +#: routes/friends.py:31 +msgid "Friend request sent!" +msgstr "" + +#: routes/friends.py:47 +msgid "Friend request accepted!" +msgstr "" + +#: routes/friends.py:49 routes/friends.py:65 +msgid "Invalid friend request." +msgstr "" + +#: routes/friends.py:63 +msgid "Friend request rejected." +msgstr "" + +#: routes/friends.py:91 +msgid "Friendship ended." +msgstr "" + +#: routes/like.py:13 routes/like.py:34 routes/post.py:63 routes/post.py:75 +msgid "Post does not exist." +msgstr "" + +#: routes/like.py:26 +msgid "Post liked." +msgstr "" + +#: routes/like.py:40 +msgid "Like removed." +msgstr "" + +#: routes/login.py:20 +msgid "Logged in successfully." +msgstr "" + +#: routes/login.py:26 +msgid "Invalid username or password." +msgstr "" + +#: routes/login.py:33 +msgid "Logged out successfully." +msgstr "" + +#: routes/login.py:58 +msgid "Username must be between 3 and 20 characters long." +msgstr "" + +#: routes/login.py:64 +msgid "Registered successfully. You can now log in." +msgstr "" + +#: routes/login.py:77 +msgid "Reset request sent to admins." +msgstr "" + +#: routes/login.py:79 +msgid "No user with this email." +msgstr "" + +#: routes/post.py:23 routes/post.py:88 +msgid "Post content is too long. Please limit it to 250 characters." +msgstr "" + +#: routes/post.py:27 routes/post.py:92 +msgid "Post content is too long. Please limit it to 500 characters." +msgstr "" + +#: routes/post.py:31 +msgid "Post created!" +msgstr "" + +#: routes/post.py:51 +msgid "You have created a new post." +msgstr "" + +#: routes/post.py:66 routes/post.py:78 +msgid "You do not have permission to edit this post." +msgstr "" + +#: routes/post.py:132 +msgid "Your post has been updated." +msgstr "" + +#: routes/post.py:137 +msgid "Post updated!" +msgstr "" + +#: routes/post.py:192 +msgid "Your post has been deleted." +msgstr "" + +#: routes/post.py:197 +msgid "Post and all uploads deleted." +msgstr "" + +#: routes/post.py:199 routes/post.py:211 +msgid "Not allowed." +msgstr "" + +#: routes/post.py:209 +msgid "Comment deleted." +msgstr "" + +#: routes/post.py:221 +msgid "You have written a comment." +msgstr "" + +#: routes/profile.py:30 +msgid "Username and email cannot be empty." +msgstr "" + +#: routes/profile.py:35 +msgid "Username already taken." +msgstr "" + +#: routes/profile.py:47 +msgid "E-Mail already taken." +msgstr "" + +#: routes/profile.py:61 +msgid "No changes made." +msgstr "" + +#: routes/profile.py:64 +msgid "Profile updated." +msgstr "" + +#: routes/support.py:26 +msgid "Support request created!" +msgstr "" + +#: routes/support.py:28 +msgid "Title and message required!" +msgstr "" + +#: routes/support.py:44 +msgid "Ticket closed." +msgstr "" + +#: routes/support.py:58 +msgid "Comment added." +msgstr "" + +#: routes/support.py:60 +msgid "Message required!" +msgstr "" + +#: routes/support.py:74 +msgid "Support ticket deleted." +msgstr "" + +#: routes/support.py:76 +msgid "Ticket not found." +msgstr "" + +#: routes/user.py:22 +msgid "Profile picture deleted." +msgstr "" + +#: routes/user.py:45 +msgid "You have changed your profile picture." +msgstr "" + +#: routes/user.py:52 +msgid "Profile picture updated." +msgstr "" + +#: routes/user.py:59 +msgid "You cannot delete the owner account." +msgstr "" + +#: routes/user.py:62 +msgid "You cannot delete an admin account." +msgstr "" + +#: routes/user.py:97 +msgid "Account and all your data deleted." +msgstr "" + +#: templates/403.html:2 templates/base.html:106 templates/index.html:2 +msgid "Home" +msgstr "" + +#: templates/403.html:12 +msgid "" +"This page is not accessible to you, you do not have the permissions to " +"view it." +msgstr "" + +#: templates/403.html:13 +msgid "You can go back from the page." +msgstr "" + +#: templates/403.html:15 templates/index.html:18 templates/index.html:20 +msgid "Go to Feed" +msgstr "" + +#: templates/admin.html:2 templates/admin.html:45 +msgid "Admin" +msgstr "" + +#: templates/admin.html:4 templates/base.html:39 +msgid "Admin Panel" +msgstr "" + +#: templates/admin.html:8 templates/admin.html:39 templates/base.html:43 +#: templates/users.html:2 +msgid "Users" +msgstr "" + +#: templates/admin.html:11 +msgid "Posts" +msgstr "" + +#: templates/admin.html:14 templates/admin.html:133 +msgid "Friendships" +msgstr "" + +#: templates/admin.html:17 templates/admin.html:176 +msgid "Comments" +msgstr "" + +#: templates/admin.html:20 templates/admin.html:213 +msgid "Uploads" +msgstr "" + +#: templates/admin.html:23 templates/admin.html:250 templates/base.html:46 +#: templates/notifications.html:2 templates/notifications.html:6 +msgid "Notifications" +msgstr "" + +#: templates/admin.html:26 +msgid "Events" +msgstr "" + +#: templates/admin.html:29 templates/admin.html:297 +msgid "Shop Orders" +msgstr "" + +#: templates/admin.html:32 templates/admin.html:324 +msgid "Reward Points" +msgstr "" + +#: templates/admin.html:43 templates/admin.html:94 templates/admin.html:180 +#: templates/admin.html:221 templates/admin.html:258 templates/admin.html:301 +#: templates/admin.html:328 templates/reset_requests.html:11 +#: templates/reset_requests.html:42 templates/reset_requests.html:67 +msgid "User" +msgstr "" + +#: templates/admin.html:44 templates/edit_profile.html:11 +#: templates/profile.html:4 templates/register.html:14 templates/setup.html:11 +msgid "Email" +msgstr "" + +#: templates/admin.html:46 +msgid "Owner" +msgstr "" + +#: templates/admin.html:47 +msgid "Profile Pic" +msgstr "" + +#: templates/admin.html:48 templates/admin.html:98 templates/admin.html:184 +#: templates/admin.html:225 templates/admin.html:330 +#: templates/reset_requests.html:13 +msgid "Actions" +msgstr "" + +#: templates/admin.html:62 templates/profile.html:12 +msgid "Delete Picture" +msgstr "" + +#: templates/admin.html:69 templates/users.html:34 +msgid "Make Admin" +msgstr "" + +#: templates/admin.html:73 templates/users.html:38 +msgid "Remove Admin" +msgstr "" + +#: templates/admin.html:78 +msgid "Delete User" +msgstr "" + +#: templates/admin.html:90 +msgid "All Posts" +msgstr "" + +#: templates/admin.html:95 templates/admin.html:182 templates/edit_post.html:7 +msgid "Content" +msgstr "" + +#: templates/admin.html:96 templates/edit_post.html:16 +msgid "Visibility" +msgstr "" + +#: templates/admin.html:97 templates/admin.html:183 templates/admin.html:260 +#: templates/support.html:26 +msgid "Created" +msgstr "" + +#: templates/admin.html:114 templates/edit_post.html:18 templates/feed.html:18 +#: templates/feed.html:86 templates/my_posts.html:61 +msgid "Public" +msgstr "" + +#: templates/admin.html:116 templates/base.html:44 templates/friends.html:2 +msgid "Friends" +msgstr "" + +#: templates/admin.html:122 +msgid "Delete Post" +msgstr "" + +#: templates/admin.html:137 +msgid "User 1" +msgstr "" + +#: templates/admin.html:138 +msgid "User 2" +msgstr "" + +#: templates/admin.html:139 templates/support.html:25 +#: templates/support_thread.html:5 +msgid "Status" +msgstr "" + +#: templates/admin.html:161 +msgid "Accepted" +msgstr "" + +#: templates/admin.html:163 +msgid "Pending" +msgstr "" + +#: templates/admin.html:165 +msgid "Rejected" +msgstr "" + +#: templates/admin.html:181 templates/feed.html:21 +msgid "Post" +msgstr "" + +#: templates/admin.html:202 +msgid "Delete Comment" +msgstr "" + +#: templates/admin.html:215 +msgid "Delete All Uploads" +msgstr "" + +#: templates/admin.html:222 +msgid "Filename" +msgstr "" + +#: templates/admin.html:223 +msgid "Type" +msgstr "" + +#: templates/admin.html:224 +msgid "Uploaded" +msgstr "" + +#: templates/admin.html:236 templates/feed.html:55 templates/my_posts.html:32 +msgid "Download" +msgstr "" + +#: templates/admin.html:238 +msgid "Delete Upload" +msgstr "" + +#: templates/admin.html:251 +msgid "Are you sure you want to delete all notifications?" +msgstr "" + +#: templates/admin.html:252 +msgid "Delete All Notifications" +msgstr "" + +#: templates/admin.html:259 +msgid "Message" +msgstr "" + +#: templates/admin.html:278 +msgid "Recent Events" +msgstr "" + +#: templates/admin.html:279 +msgid "Are you sure you want to delete all events?" +msgstr "" + +#: templates/admin.html:280 +msgid "Delete All Events" +msgstr "" + +#: templates/admin.html:302 +msgid "Item" +msgstr "" + +#: templates/admin.html:329 templates/shop.html:17 +msgid "Points" +msgstr "" + +#: templates/admin.html:347 +msgid "Add Points" +msgstr "" + +#: templates/admin.html:350 +msgid "Remove Points" +msgstr "" + +#: templates/admin_set_password.html:2 +msgid "Set New Password" +msgstr "" + +#: templates/admin_set_password.html:4 +#, python-format +msgid "Set New Password for %(username)s" +msgstr "" + +#: templates/admin_set_password.html:7 +msgid "New Password" +msgstr "" + +#: templates/admin_set_password.html:10 templates/reset_requests.html:28 +msgid "Set Password" +msgstr "" + +#: templates/base.html:40 +msgid "Reset Requests" +msgstr "" + +#: templates/base.html:42 templates/base.html:51 templates/feed.html:2 +msgid "Feed" +msgstr "" + +#: templates/base.html:45 templates/my_posts.html:2 templates/my_posts.html:4 +msgid "My Posts" +msgstr "" + +#: templates/base.html:47 templates/profile.html:2 +msgid "Profile" +msgstr "" + +#: templates/base.html:48 +msgid "Logout" +msgstr "" + +#: templates/base.html:49 templates/support.html:2 templates/support.html:15 +msgid "Support" +msgstr "" + +#: templates/base.html:52 templates/index.html:16 templates/login.html:2 +#: templates/login.html:7 templates/login.html:18 templates/register.html:30 +#: templates/reset_password.html:17 +msgid "Login" +msgstr "" + +#: templates/base.html:53 templates/index.html:17 templates/login.html:21 +#: templates/register.html:2 templates/register.html:7 +#: templates/register.html:26 +msgid "Register" +msgstr "" + +#: templates/base.html:57 +msgid "Theme" +msgstr "" + +#: templates/base.html:90 +#, python-format +msgid "Welcome, %(username)s!" +msgstr "" + +#: templates/base.html:94 +msgid "You are logged in as an admin." +msgstr "" + +#: templates/base.html:107 templates/privacy_policy.html:2 +#: templates/privacy_policy.html:4 +msgid "Privacy Policy" +msgstr "" + +#: templates/base.html:108 templates/credits.html:2 templates/credits.html:4 +msgid "Credits" +msgstr "" + +#: templates/credits.html:5 +msgid "This project was developed by" +msgstr "" + +#: templates/credits.html:5 +msgid "Special thanks to all contributors and supporters." +msgstr "" + +#: templates/credits.html:6 +msgid "Translators:" +msgstr "" + +#: templates/credits.html:8 +msgid "German:" +msgstr "" + +#: templates/credits.html:9 +msgid "English:" +msgstr "" + +#: templates/credits.html:11 +msgid "" +"Special thanks to the open-source community for their invaluable " +"resources and tools." +msgstr "" + +#: templates/credits.html:12 +msgid "Design inspired by various open-source projects and communities." +msgstr "" + +#: templates/credits.html:13 +msgid "Backend powered by Flask and SQLAlchemy." +msgstr "" + +#: templates/credits.html:14 +msgid "Frontend built with Bootstrap and jQuery." +msgstr "" + +#: templates/credits.html:15 +msgid "Icons by FontAwesome and other open-source resources." +msgstr "" + +#: templates/credits.html:16 +msgid "Hosted on a secure and reliable platform." +msgstr "" + +#: templates/credits.html:17 +msgid "If you would like to contribute, please reach out to us." +msgstr "" + +#: templates/credits.html:18 templates/privacy_policy.html:14 +msgid "GitHub Repository" +msgstr "" + +#: templates/credits.html:19 templates/privacy_policy.html:15 +msgid "Thank you for using our application!" +msgstr "" + +#: templates/discord_register.html:2 +msgid "Discord Registration" +msgstr "" + +#: templates/discord_register.html:4 +msgid "Complete Registration" +msgstr "" + +#: templates/discord_register.html:5 +msgid "Welcome," +msgstr "" + +#: templates/discord_register.html:6 +msgid "Please set a password for your account:" +msgstr "" + +#: templates/discord_register.html:12 templates/login.html:14 +#: templates/register.html:18 templates/setup.html:15 +msgid "Password" +msgstr "" + +#: templates/discord_register.html:16 templates/register.html:22 +#: templates/setup.html:19 +msgid "Confirm Password" +msgstr "" + +#: templates/discord_register.html:19 +msgid "Create Account" +msgstr "" + +#: templates/discord_register.html:20 templates/edit_post.html:31 +msgid "Cancel" +msgstr "" + +#: templates/edit_post.html:2 templates/edit_post.html:4 +msgid "Edit Post" +msgstr "" + +#: templates/edit_post.html:9 templates/feed.html:7 +msgid "Limit: 250" +msgstr "" + +#: templates/edit_post.html:11 templates/feed.html:9 +msgid "Limit: 500" +msgstr "" + +#: templates/edit_post.html:19 templates/feed.html:19 templates/feed.html:84 +#: templates/my_posts.html:59 +msgid "Friends only" +msgstr "" + +#: templates/edit_post.html:23 +msgid "Upload Files" +msgstr "" + +#: templates/edit_post.html:28 templates/feed.html:16 +msgid "You can upload images, videos, audio files, or documents." +msgstr "" + +#: templates/edit_post.html:30 +msgid "Update Post" +msgstr "" + +#: templates/edit_post.html:35 +msgid "Current Uploads" +msgstr "" + +#: templates/edit_post.html:51 +msgid "No uploads found for this post." +msgstr "" + +#: templates/edit_profile.html:2 templates/edit_profile.html:4 +#: templates/profile.html:16 +msgid "Edit Profile" +msgstr "" + +#: templates/edit_profile.html:7 templates/login.html:10 +#: templates/register.html:10 templates/reset_password.html:10 +#: templates/setup.html:7 +msgid "Username" +msgstr "" + +#: templates/edit_profile.html:15 +msgid "New Password (optional)" +msgstr "" + +#: templates/edit_profile.html:19 +msgid "Confirm New Password" +msgstr "" + +#: templates/edit_profile.html:22 +msgid "Save" +msgstr "" + +#: templates/feed.html:11 +msgid "What's on your mind?" +msgstr "" + +#: templates/feed.html:25 +msgid "Please" +msgstr "" + +#: templates/feed.html:25 +msgid "log in" +msgstr "" + +#: templates/feed.html:25 +msgid "to create a post." +msgstr "" + +#: templates/feed.html:61 templates/my_posts.html:38 +msgid "Download Video" +msgstr "" + +#: templates/feed.html:64 templates/my_posts.html:41 +msgid "Download Audio" +msgstr "" + +#: templates/feed.html:70 templates/my_posts.html:47 +msgid "Like" +msgstr "" + +#: templates/feed.html:73 templates/my_posts.html:50 +msgid "Unlike" +msgstr "" + +#: templates/feed.html:77 templates/feed.html:107 templates/my_posts.html:53 +#: templates/my_posts.html:77 templates/notifications.html:25 +#: templates/support_thread.html:35 +msgid "Delete" +msgstr "" + +#: templates/feed.html:80 templates/my_posts.html:56 +msgid "Edit" +msgstr "" + +#: templates/feed.html:91 templates/my_posts.html:66 +msgid "Comment..." +msgstr "" + +#: templates/feed.html:95 +msgid "Please login to comment." +msgstr "" + +#: templates/feed.html:118 templates/my_posts.html:88 +msgid "No posts available." +msgstr "" + +#: templates/friends.html:4 +msgid "Your Friends" +msgstr "" + +#: templates/friends.html:16 +msgid "Remove Friend" +msgstr "" + +#: templates/friends.html:20 +msgid "No friends yet." +msgstr "" + +#: templates/friends.html:23 +msgid "Friend Requests" +msgstr "" + +#: templates/friends.html:36 +msgid "Accept" +msgstr "" + +#: templates/friends.html:39 templates/reset_requests.html:30 +msgid "Reject" +msgstr "" + +#: templates/friends.html:44 +msgid "No new requests" +msgstr "" + +#: templates/index.html:12 +msgid "" +"MiniFacebook is a minimalist social network for sharing posts, images, " +"and messages with friends." +msgstr "" + +#: templates/index.html:13 +msgid "Fast, simple, data-saving - and with" +msgstr "" + +#: templates/index.html:13 templates/index.html:37 +msgid "Dark Mode!" +msgstr "" + +#: templates/index.html:27 +msgid "About MiniFacebook" +msgstr "" + +#: templates/index.html:30 +msgid "Share posts, images & videos" +msgstr "" + +#: templates/index.html:31 +msgid "Friendships & notifications" +msgstr "" + +#: templates/index.html:32 +msgid "Modern" +msgstr "" + +#: templates/index.html:32 +msgid "Dark/Light Mode" +msgstr "" + +#: templates/index.html:33 +msgid "Open Source & privacy-friendly" +msgstr "" + +#: templates/index.html:36 +msgid "" +"is a private, data-saving network for you and your friends. No ads, no " +"data sharing - just fun in sharing and communicating." +msgstr "" + +#: templates/index.html:37 +msgid "Developed with love," +msgstr "" + +#: templates/login.html:22 +msgid "Forgot password?" +msgstr "" + +#: templates/login.html:27 templates/register.html:34 +msgid "Login with Discord" +msgstr "" + +#: templates/notifications.html:11 +msgid "Delete all" +msgstr "" + +#: templates/notifications.html:29 +msgid "No notifications" +msgstr "" + +#: templates/privacy_policy.html:5 +msgid "" +"Your privacy is important to us. This privacy policy explains how we " +"collect, use, and protect your information when you use our application." +msgstr "" + +#: templates/privacy_policy.html:6 +msgid "" +"We collect personal information that you provide to us, such as your " +"name, email address, and any other information you choose to share." +msgstr "" + +#: templates/privacy_policy.html:7 +msgid "" +"We use this information to provide and improve our services, communicate " +"with you, and personalize your experience." +msgstr "" + +#: templates/privacy_policy.html:8 +msgid "" +"We do not share your personal information with third parties without your" +" consent, except as required by law or to protect our rights." +msgstr "" + +#: templates/privacy_policy.html:9 +msgid "" +"We implement security measures to protect your information from " +"unauthorized access, alteration, disclosure, or destruction." +msgstr "" + +#: templates/privacy_policy.html:10 +msgid "" +"You have the right to access, correct, or delete your personal " +"information at any time. Please contact us if you wish to exercise these " +"rights." +msgstr "" + +#: templates/privacy_policy.html:11 +msgid "" +"We may update this privacy policy from time to time. We will notify you " +"of any changes by posting the new privacy policy on this page." +msgstr "" + +#: templates/privacy_policy.html:12 +msgid "" +"By using our application, you agree to the terms of this privacy policy. " +"If you do not agree, please do not use our application." +msgstr "" + +#: templates/privacy_policy.html:13 +msgid "" +"If you have any questions or concerns about this privacy policy, please " +"contact us." +msgstr "" + +#: templates/profile.html:8 +msgid "Upload Picture" +msgstr "" + +#: templates/profile.html:15 templates/shop.html:2 templates/shop.html:4 +msgid "Shop" +msgstr "" + +#: templates/profile.html:18 +msgid "Delete Account" +msgstr "" + +#: templates/profile.html:22 +msgid "Link Discord Account" +msgstr "" + +#: templates/profile.html:25 +msgid "Discord Linked" +msgstr "" + +#: templates/profile.html:28 +msgid "Unlink Discord" +msgstr "" + +#: templates/register.html:29 +msgid "Already have an account?" +msgstr "" + +#: templates/reset_password.html:2 templates/reset_password.html:7 +msgid "Reset Password" +msgstr "" + +#: templates/reset_password.html:14 +msgid "Request Reset" +msgstr "" + +#: templates/reset_requests.html:2 templates/reset_requests.html:5 +msgid "Password Reset Requests" +msgstr "" + +#: templates/reset_requests.html:6 +msgid "Delete All" +msgstr "" + +#: templates/reset_requests.html:12 templates/reset_requests.html:43 +#: templates/reset_requests.html:68 +msgid "Requested At" +msgstr "" + +#: templates/reset_requests.html:38 +msgid "Completed Requests" +msgstr "" + +#: templates/reset_requests.html:63 +msgid "Rejected Requests" +msgstr "" + +#: templates/reset_requests.html:88 +msgid "No open reset requests." +msgstr "" + +#: templates/secret.html:2 +msgid "Secret" +msgstr "" + +#: templates/setup.html:2 +msgid "Admin Setup" +msgstr "" + +#: templates/setup.html:4 +msgid "Admin Account Setup" +msgstr "" + +#: templates/setup.html:22 +msgid "Create Admin" +msgstr "" + +#: templates/shop.html:5 +msgid "Deine Reward-Punkte:" +msgstr "" + +#: templates/shop.html:19 +msgid "Bought" +msgstr "" + +#: templates/shop.html:24 +msgid "Buy" +msgstr "" + +#: templates/support.html:5 +msgid "If you have any questions or need assistance, please contact us at:" +msgstr "" + +#: templates/support.html:6 +msgid "Github:" +msgstr "" + +#: templates/support.html:11 +msgid "Wipe Server" +msgstr "" + +#: templates/support.html:17 templates/support.html:24 +msgid "Title" +msgstr "" + +#: templates/support.html:18 +msgid "Describe your issue..." +msgstr "" + +#: templates/support.html:19 +msgid "Create Ticket" +msgstr "" + +#: templates/support.html:36 templates/support_thread.html:7 +msgid "Open" +msgstr "" + +#: templates/support.html:38 templates/support_thread.html:9 +msgid "Closed" +msgstr "" + +#: templates/support.html:43 +msgid "View" +msgstr "" + +#: templates/support_thread.html:23 +msgid "Write a message..." +msgstr "" + +#: templates/support_thread.html:24 +msgid "Send" +msgstr "" + +#: templates/support_thread.html:27 +msgid "Close Ticket" +msgstr "" + +#: templates/support_thread.html:30 +msgid "This ticket is closed." +msgstr "" + +#: templates/support_thread.html:39 +msgid "Back to Support" +msgstr "" + +#: templates/users.html:4 +msgid "All Users" +msgstr "" + +#: templates/users.html:20 +msgid "Request sent" +msgstr "" + +#: templates/users.html:22 +msgid "Request received" +msgstr "" + +#: templates/users.html:24 +msgid "Friend" +msgstr "" + +#: templates/users.html:27 +msgid "Add Friend" +msgstr "" + diff --git a/translations/en/LC_MESSAGES/messages.po b/translations/en/LC_MESSAGES/messages.po new file mode 100644 index 0000000..1cc095f --- /dev/null +++ b/translations/en/LC_MESSAGES/messages.po @@ -0,0 +1,1166 @@ +# English translations for PROJECT. +# Copyright (C) 2025 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2025-09-27 20:09+0200\n" +"PO-Revision-Date: 2025-09-27 20:09+0200\n" +"Last-Translator: FULL NAME \n" +"Language: en\n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.17.0\n" + +#: main.py:34 +msgid "Please log in to access this page." +msgstr "" + +#: main.py:117 routes/discord.py:44 routes/login.py:46 routes/profile.py:56 +msgid "Passwords do not match." +msgstr "" + +#: main.py:119 routes/login.py:48 +msgid "Username already exists." +msgstr "" + +#: main.py:121 routes/login.py:50 +msgid "E-Mail already exists." +msgstr "" + +#: main.py:123 routes/discord.py:41 routes/login.py:52 routes/profile.py:53 +msgid "Password must be at least 8 characters long." +msgstr "" + +#: main.py:125 routes/login.py:54 routes/profile.py:44 +msgid "Invalid email address." +msgstr "" + +#: main.py:127 routes/login.py:56 routes/profile.py:38 +msgid "Invalid username. Only alphanumeric characters are allowed." +msgstr "" + +#: main.py:133 +msgid "Admin account created. You can now log in." +msgstr "" + +#: main.py:159 +msgid "Already purchased!" +msgstr "" + +#: main.py:167 +msgid "Not enough points!" +msgstr "" + +#: routes/admin.py:18 +msgid "All password reset requests have been deleted." +msgstr "" + +#: routes/admin.py:65 +msgid "Request rejected." +msgstr "" + +#: routes/admin.py:79 +msgid "Password too short." +msgstr "" + +#: routes/admin.py:115 +msgid "Post and associated files deleted." +msgstr "" + +#: routes/admin.py:123 +msgid "Cannot delete the owner account." +msgstr "" + +#: routes/admin.py:158 +msgid "User deleted." +msgstr "" + +#: routes/admin.py:160 +msgid "Cannot delete admin or user not found." +msgstr "" + +#: routes/admin.py:186 routes/notifications.py:14 +msgid "All notifications have been deleted." +msgstr "" + +#: routes/admin.py:196 +msgid "All events have been deleted." +msgstr "" + +#: routes/admin.py:214 +msgid "Upload deleted." +msgstr "" + +#: routes/admin.py:232 +msgid "All uploads have been deleted." +msgstr "" + +#: routes/admin.py:244 +msgid "No Points entered!" +msgstr "" + +#: routes/admin.py:252 +msgid "Points added!" +msgstr "" + +#: routes/admin.py:258 +msgid "Points removed!" +msgstr "" + +#: routes/admin.py:260 +msgid "The user has not enough points to take!" +msgstr "" + +#: routes/admin.py:286 +msgid "Owner cannot be removed!" +msgstr "" + +#: routes/admin.py:329 +msgid "All Data has been deleted." +msgstr "" + +#: routes/discord.py:24 +msgid "Logged in with Discord." +msgstr "" + +#: routes/discord.py:27 +msgid "No account linked with this Discord. Please register." +msgstr "" + +#: routes/discord.py:47 +msgid "Username already exists. Please Report It." +msgstr "" + +#: routes/discord.py:60 +msgid "Account created and logged in with Discord." +msgstr "" + +#: routes/discord.py:77 +msgid "Discord account linked!" +msgstr "" + +#: routes/discord.py:86 +msgid "Discord account unlinked!" +msgstr "" + +#: routes/friends.py:12 +msgid "You cannot add yourself as a friend." +msgstr "" + +#: routes/friends.py:16 +msgid "Friend request already sent." +msgstr "" + +#: routes/friends.py:31 +msgid "Friend request sent!" +msgstr "" + +#: routes/friends.py:47 +msgid "Friend request accepted!" +msgstr "" + +#: routes/friends.py:49 routes/friends.py:65 +msgid "Invalid friend request." +msgstr "" + +#: routes/friends.py:63 +msgid "Friend request rejected." +msgstr "" + +#: routes/friends.py:91 +msgid "Friendship ended." +msgstr "" + +#: routes/like.py:13 routes/like.py:34 routes/post.py:63 routes/post.py:75 +msgid "Post does not exist." +msgstr "" + +#: routes/like.py:26 +msgid "Post liked." +msgstr "" + +#: routes/like.py:40 +msgid "Like removed." +msgstr "" + +#: routes/login.py:20 +msgid "Logged in successfully." +msgstr "" + +#: routes/login.py:26 +msgid "Invalid username or password." +msgstr "" + +#: routes/login.py:33 +msgid "Logged out successfully." +msgstr "" + +#: routes/login.py:58 +msgid "Username must be between 3 and 20 characters long." +msgstr "" + +#: routes/login.py:64 +msgid "Registered successfully. You can now log in." +msgstr "" + +#: routes/login.py:77 +msgid "Reset request sent to admins." +msgstr "" + +#: routes/login.py:79 +msgid "No user with this email." +msgstr "" + +#: routes/post.py:23 routes/post.py:88 +msgid "Post content is too long. Please limit it to 250 characters." +msgstr "" + +#: routes/post.py:27 routes/post.py:92 +msgid "Post content is too long. Please limit it to 500 characters." +msgstr "" + +#: routes/post.py:31 +msgid "Post created!" +msgstr "" + +#: routes/post.py:51 +msgid "You have created a new post." +msgstr "" + +#: routes/post.py:66 routes/post.py:78 +msgid "You do not have permission to edit this post." +msgstr "" + +#: routes/post.py:132 +msgid "Your post has been updated." +msgstr "" + +#: routes/post.py:137 +msgid "Post updated!" +msgstr "" + +#: routes/post.py:192 +msgid "Your post has been deleted." +msgstr "" + +#: routes/post.py:197 +msgid "Post and all uploads deleted." +msgstr "" + +#: routes/post.py:199 routes/post.py:211 +msgid "Not allowed." +msgstr "" + +#: routes/post.py:209 +msgid "Comment deleted." +msgstr "" + +#: routes/post.py:221 +msgid "You have written a comment." +msgstr "" + +#: routes/profile.py:30 +msgid "Username and email cannot be empty." +msgstr "" + +#: routes/profile.py:35 +msgid "Username already taken." +msgstr "" + +#: routes/profile.py:47 +msgid "E-Mail already taken." +msgstr "" + +#: routes/profile.py:61 +msgid "No changes made." +msgstr "" + +#: routes/profile.py:64 +msgid "Profile updated." +msgstr "" + +#: routes/support.py:26 +msgid "Support request created!" +msgstr "" + +#: routes/support.py:28 +msgid "Title and message required!" +msgstr "" + +#: routes/support.py:44 +msgid "Ticket closed." +msgstr "" + +#: routes/support.py:58 +msgid "Comment added." +msgstr "" + +#: routes/support.py:60 +msgid "Message required!" +msgstr "" + +#: routes/support.py:74 +msgid "Support ticket deleted." +msgstr "" + +#: routes/support.py:76 +msgid "Ticket not found." +msgstr "" + +#: routes/user.py:22 +msgid "Profile picture deleted." +msgstr "" + +#: routes/user.py:45 +msgid "You have changed your profile picture." +msgstr "" + +#: routes/user.py:52 +msgid "Profile picture updated." +msgstr "" + +#: routes/user.py:59 +msgid "You cannot delete the owner account." +msgstr "" + +#: routes/user.py:62 +msgid "You cannot delete an admin account." +msgstr "" + +#: routes/user.py:97 +msgid "Account and all your data deleted." +msgstr "" + +#: templates/403.html:2 templates/base.html:106 templates/index.html:2 +msgid "Home" +msgstr "" + +#: templates/403.html:12 +msgid "" +"This page is not accessible to you, you do not have the permissions to " +"view it." +msgstr "" + +#: templates/403.html:13 +msgid "You can go back from the page." +msgstr "" + +#: templates/403.html:15 templates/index.html:18 templates/index.html:20 +msgid "Go to Feed" +msgstr "" + +#: templates/admin.html:2 templates/admin.html:45 +msgid "Admin" +msgstr "" + +#: templates/admin.html:4 templates/base.html:39 +msgid "Admin Panel" +msgstr "" + +#: templates/admin.html:8 templates/admin.html:39 templates/base.html:43 +#: templates/users.html:2 +msgid "Users" +msgstr "" + +#: templates/admin.html:11 +msgid "Posts" +msgstr "" + +#: templates/admin.html:14 templates/admin.html:133 +msgid "Friendships" +msgstr "" + +#: templates/admin.html:17 templates/admin.html:176 +msgid "Comments" +msgstr "" + +#: templates/admin.html:20 templates/admin.html:213 +msgid "Uploads" +msgstr "" + +#: templates/admin.html:23 templates/admin.html:250 templates/base.html:46 +#: templates/notifications.html:2 templates/notifications.html:6 +msgid "Notifications" +msgstr "" + +#: templates/admin.html:26 +msgid "Events" +msgstr "" + +#: templates/admin.html:29 templates/admin.html:297 +msgid "Shop Orders" +msgstr "" + +#: templates/admin.html:32 templates/admin.html:324 +msgid "Reward Points" +msgstr "" + +#: templates/admin.html:43 templates/admin.html:94 templates/admin.html:180 +#: templates/admin.html:221 templates/admin.html:258 templates/admin.html:301 +#: templates/admin.html:328 templates/reset_requests.html:11 +#: templates/reset_requests.html:42 templates/reset_requests.html:67 +msgid "User" +msgstr "" + +#: templates/admin.html:44 templates/edit_profile.html:11 +#: templates/profile.html:4 templates/register.html:14 templates/setup.html:11 +msgid "Email" +msgstr "" + +#: templates/admin.html:46 +msgid "Owner" +msgstr "" + +#: templates/admin.html:47 +msgid "Profile Pic" +msgstr "" + +#: templates/admin.html:48 templates/admin.html:98 templates/admin.html:184 +#: templates/admin.html:225 templates/admin.html:330 +#: templates/reset_requests.html:13 +msgid "Actions" +msgstr "" + +#: templates/admin.html:62 templates/profile.html:12 +msgid "Delete Picture" +msgstr "" + +#: templates/admin.html:69 templates/users.html:34 +msgid "Make Admin" +msgstr "" + +#: templates/admin.html:73 templates/users.html:38 +msgid "Remove Admin" +msgstr "" + +#: templates/admin.html:78 +msgid "Delete User" +msgstr "" + +#: templates/admin.html:90 +msgid "All Posts" +msgstr "" + +#: templates/admin.html:95 templates/admin.html:182 templates/edit_post.html:7 +msgid "Content" +msgstr "" + +#: templates/admin.html:96 templates/edit_post.html:16 +msgid "Visibility" +msgstr "" + +#: templates/admin.html:97 templates/admin.html:183 templates/admin.html:260 +#: templates/support.html:26 +msgid "Created" +msgstr "" + +#: templates/admin.html:114 templates/edit_post.html:18 templates/feed.html:18 +#: templates/feed.html:86 templates/my_posts.html:61 +msgid "Public" +msgstr "" + +#: templates/admin.html:116 templates/base.html:44 templates/friends.html:2 +msgid "Friends" +msgstr "" + +#: templates/admin.html:122 +msgid "Delete Post" +msgstr "" + +#: templates/admin.html:137 +msgid "User 1" +msgstr "" + +#: templates/admin.html:138 +msgid "User 2" +msgstr "" + +#: templates/admin.html:139 templates/support.html:25 +#: templates/support_thread.html:5 +msgid "Status" +msgstr "" + +#: templates/admin.html:161 +msgid "Accepted" +msgstr "" + +#: templates/admin.html:163 +msgid "Pending" +msgstr "" + +#: templates/admin.html:165 +msgid "Rejected" +msgstr "" + +#: templates/admin.html:181 templates/feed.html:21 +msgid "Post" +msgstr "" + +#: templates/admin.html:202 +msgid "Delete Comment" +msgstr "" + +#: templates/admin.html:215 +msgid "Delete All Uploads" +msgstr "" + +#: templates/admin.html:222 +msgid "Filename" +msgstr "" + +#: templates/admin.html:223 +msgid "Type" +msgstr "" + +#: templates/admin.html:224 +msgid "Uploaded" +msgstr "" + +#: templates/admin.html:236 templates/feed.html:55 templates/my_posts.html:32 +msgid "Download" +msgstr "" + +#: templates/admin.html:238 +msgid "Delete Upload" +msgstr "" + +#: templates/admin.html:251 +msgid "Are you sure you want to delete all notifications?" +msgstr "" + +#: templates/admin.html:252 +msgid "Delete All Notifications" +msgstr "" + +#: templates/admin.html:259 +msgid "Message" +msgstr "" + +#: templates/admin.html:278 +msgid "Recent Events" +msgstr "" + +#: templates/admin.html:279 +msgid "Are you sure you want to delete all events?" +msgstr "" + +#: templates/admin.html:280 +msgid "Delete All Events" +msgstr "" + +#: templates/admin.html:302 +msgid "Item" +msgstr "" + +#: templates/admin.html:329 templates/shop.html:17 +msgid "Points" +msgstr "" + +#: templates/admin.html:347 +msgid "Add Points" +msgstr "" + +#: templates/admin.html:350 +msgid "Remove Points" +msgstr "" + +#: templates/admin_set_password.html:2 +msgid "Set New Password" +msgstr "" + +#: templates/admin_set_password.html:4 +#, python-format +msgid "Set New Password for %(username)s" +msgstr "" + +#: templates/admin_set_password.html:7 +msgid "New Password" +msgstr "" + +#: templates/admin_set_password.html:10 templates/reset_requests.html:28 +msgid "Set Password" +msgstr "" + +#: templates/base.html:40 +msgid "Reset Requests" +msgstr "" + +#: templates/base.html:42 templates/base.html:51 templates/feed.html:2 +msgid "Feed" +msgstr "" + +#: templates/base.html:45 templates/my_posts.html:2 templates/my_posts.html:4 +msgid "My Posts" +msgstr "" + +#: templates/base.html:47 templates/profile.html:2 +msgid "Profile" +msgstr "" + +#: templates/base.html:48 +msgid "Logout" +msgstr "" + +#: templates/base.html:49 templates/support.html:2 templates/support.html:15 +msgid "Support" +msgstr "" + +#: templates/base.html:52 templates/index.html:16 templates/login.html:2 +#: templates/login.html:7 templates/login.html:18 templates/register.html:30 +#: templates/reset_password.html:17 +msgid "Login" +msgstr "" + +#: templates/base.html:53 templates/index.html:17 templates/login.html:21 +#: templates/register.html:2 templates/register.html:7 +#: templates/register.html:26 +msgid "Register" +msgstr "" + +#: templates/base.html:57 +msgid "Theme" +msgstr "" + +#: templates/base.html:90 +#, python-format +msgid "Welcome, %(username)s!" +msgstr "" + +#: templates/base.html:94 +msgid "You are logged in as an admin." +msgstr "" + +#: templates/base.html:107 templates/privacy_policy.html:2 +#: templates/privacy_policy.html:4 +msgid "Privacy Policy" +msgstr "" + +#: templates/base.html:108 templates/credits.html:2 templates/credits.html:4 +msgid "Credits" +msgstr "" + +#: templates/credits.html:5 +msgid "This project was developed by" +msgstr "" + +#: templates/credits.html:5 +msgid "Special thanks to all contributors and supporters." +msgstr "" + +#: templates/credits.html:6 +msgid "Translators:" +msgstr "" + +#: templates/credits.html:8 +msgid "German:" +msgstr "" + +#: templates/credits.html:9 +msgid "English:" +msgstr "" + +#: templates/credits.html:11 +msgid "" +"Special thanks to the open-source community for their invaluable " +"resources and tools." +msgstr "" + +#: templates/credits.html:12 +msgid "Design inspired by various open-source projects and communities." +msgstr "" + +#: templates/credits.html:13 +msgid "Backend powered by Flask and SQLAlchemy." +msgstr "" + +#: templates/credits.html:14 +msgid "Frontend built with Bootstrap and jQuery." +msgstr "" + +#: templates/credits.html:15 +msgid "Icons by FontAwesome and other open-source resources." +msgstr "" + +#: templates/credits.html:16 +msgid "Hosted on a secure and reliable platform." +msgstr "" + +#: templates/credits.html:17 +msgid "If you would like to contribute, please reach out to us." +msgstr "" + +#: templates/credits.html:18 templates/privacy_policy.html:14 +msgid "GitHub Repository" +msgstr "" + +#: templates/credits.html:19 templates/privacy_policy.html:15 +msgid "Thank you for using our application!" +msgstr "" + +#: templates/discord_register.html:2 +msgid "Discord Registration" +msgstr "" + +#: templates/discord_register.html:4 +msgid "Complete Registration" +msgstr "" + +#: templates/discord_register.html:5 +msgid "Welcome," +msgstr "" + +#: templates/discord_register.html:6 +msgid "Please set a password for your account:" +msgstr "" + +#: templates/discord_register.html:12 templates/login.html:14 +#: templates/register.html:18 templates/setup.html:15 +msgid "Password" +msgstr "" + +#: templates/discord_register.html:16 templates/register.html:22 +#: templates/setup.html:19 +msgid "Confirm Password" +msgstr "" + +#: templates/discord_register.html:19 +msgid "Create Account" +msgstr "" + +#: templates/discord_register.html:20 templates/edit_post.html:31 +msgid "Cancel" +msgstr "" + +#: templates/edit_post.html:2 templates/edit_post.html:4 +msgid "Edit Post" +msgstr "" + +#: templates/edit_post.html:9 templates/feed.html:7 +msgid "Limit: 250" +msgstr "" + +#: templates/edit_post.html:11 templates/feed.html:9 +msgid "Limit: 500" +msgstr "" + +#: templates/edit_post.html:19 templates/feed.html:19 templates/feed.html:84 +#: templates/my_posts.html:59 +msgid "Friends only" +msgstr "" + +#: templates/edit_post.html:23 +msgid "Upload Files" +msgstr "" + +#: templates/edit_post.html:28 templates/feed.html:16 +msgid "You can upload images, videos, audio files, or documents." +msgstr "" + +#: templates/edit_post.html:30 +msgid "Update Post" +msgstr "" + +#: templates/edit_post.html:35 +msgid "Current Uploads" +msgstr "" + +#: templates/edit_post.html:51 +msgid "No uploads found for this post." +msgstr "" + +#: templates/edit_profile.html:2 templates/edit_profile.html:4 +#: templates/profile.html:16 +msgid "Edit Profile" +msgstr "" + +#: templates/edit_profile.html:7 templates/login.html:10 +#: templates/register.html:10 templates/reset_password.html:10 +#: templates/setup.html:7 +msgid "Username" +msgstr "" + +#: templates/edit_profile.html:15 +msgid "New Password (optional)" +msgstr "" + +#: templates/edit_profile.html:19 +msgid "Confirm New Password" +msgstr "" + +#: templates/edit_profile.html:22 +msgid "Save" +msgstr "" + +#: templates/feed.html:11 +msgid "What's on your mind?" +msgstr "" + +#: templates/feed.html:25 +msgid "Please" +msgstr "" + +#: templates/feed.html:25 +msgid "log in" +msgstr "" + +#: templates/feed.html:25 +msgid "to create a post." +msgstr "" + +#: templates/feed.html:61 templates/my_posts.html:38 +msgid "Download Video" +msgstr "" + +#: templates/feed.html:64 templates/my_posts.html:41 +msgid "Download Audio" +msgstr "" + +#: templates/feed.html:70 templates/my_posts.html:47 +msgid "Like" +msgstr "" + +#: templates/feed.html:73 templates/my_posts.html:50 +msgid "Unlike" +msgstr "" + +#: templates/feed.html:77 templates/feed.html:107 templates/my_posts.html:53 +#: templates/my_posts.html:77 templates/notifications.html:25 +#: templates/support_thread.html:35 +msgid "Delete" +msgstr "" + +#: templates/feed.html:80 templates/my_posts.html:56 +msgid "Edit" +msgstr "" + +#: templates/feed.html:91 templates/my_posts.html:66 +msgid "Comment..." +msgstr "" + +#: templates/feed.html:95 +msgid "Please login to comment." +msgstr "" + +#: templates/feed.html:118 templates/my_posts.html:88 +msgid "No posts available." +msgstr "" + +#: templates/friends.html:4 +msgid "Your Friends" +msgstr "" + +#: templates/friends.html:16 +msgid "Remove Friend" +msgstr "" + +#: templates/friends.html:20 +msgid "No friends yet." +msgstr "" + +#: templates/friends.html:23 +msgid "Friend Requests" +msgstr "" + +#: templates/friends.html:36 +msgid "Accept" +msgstr "" + +#: templates/friends.html:39 templates/reset_requests.html:30 +msgid "Reject" +msgstr "" + +#: templates/friends.html:44 +msgid "No new requests" +msgstr "" + +#: templates/index.html:12 +msgid "" +"MiniFacebook is a minimalist social network for sharing posts, images, " +"and messages with friends." +msgstr "" + +#: templates/index.html:13 +msgid "Fast, simple, data-saving - and with" +msgstr "" + +#: templates/index.html:13 templates/index.html:37 +msgid "Dark Mode!" +msgstr "" + +#: templates/index.html:27 +msgid "About MiniFacebook" +msgstr "" + +#: templates/index.html:30 +msgid "Share posts, images & videos" +msgstr "" + +#: templates/index.html:31 +msgid "Friendships & notifications" +msgstr "" + +#: templates/index.html:32 +msgid "Modern" +msgstr "" + +#: templates/index.html:32 +msgid "Dark/Light Mode" +msgstr "" + +#: templates/index.html:33 +msgid "Open Source & privacy-friendly" +msgstr "" + +#: templates/index.html:36 +msgid "" +"is a private, data-saving network for you and your friends. No ads, no " +"data sharing - just fun in sharing and communicating." +msgstr "" + +#: templates/index.html:37 +msgid "Developed with love," +msgstr "" + +#: templates/login.html:22 +msgid "Forgot password?" +msgstr "" + +#: templates/login.html:27 templates/register.html:34 +msgid "Login with Discord" +msgstr "" + +#: templates/notifications.html:11 +msgid "Delete all" +msgstr "" + +#: templates/notifications.html:29 +msgid "No notifications" +msgstr "" + +#: templates/privacy_policy.html:5 +msgid "" +"Your privacy is important to us. This privacy policy explains how we " +"collect, use, and protect your information when you use our application." +msgstr "" + +#: templates/privacy_policy.html:6 +msgid "" +"We collect personal information that you provide to us, such as your " +"name, email address, and any other information you choose to share." +msgstr "" + +#: templates/privacy_policy.html:7 +msgid "" +"We use this information to provide and improve our services, communicate " +"with you, and personalize your experience." +msgstr "" + +#: templates/privacy_policy.html:8 +msgid "" +"We do not share your personal information with third parties without your" +" consent, except as required by law or to protect our rights." +msgstr "" + +#: templates/privacy_policy.html:9 +msgid "" +"We implement security measures to protect your information from " +"unauthorized access, alteration, disclosure, or destruction." +msgstr "" + +#: templates/privacy_policy.html:10 +msgid "" +"You have the right to access, correct, or delete your personal " +"information at any time. Please contact us if you wish to exercise these " +"rights." +msgstr "" + +#: templates/privacy_policy.html:11 +msgid "" +"We may update this privacy policy from time to time. We will notify you " +"of any changes by posting the new privacy policy on this page." +msgstr "" + +#: templates/privacy_policy.html:12 +msgid "" +"By using our application, you agree to the terms of this privacy policy. " +"If you do not agree, please do not use our application." +msgstr "" + +#: templates/privacy_policy.html:13 +msgid "" +"If you have any questions or concerns about this privacy policy, please " +"contact us." +msgstr "" + +#: templates/profile.html:8 +msgid "Upload Picture" +msgstr "" + +#: templates/profile.html:15 templates/shop.html:2 templates/shop.html:4 +msgid "Shop" +msgstr "" + +#: templates/profile.html:18 +msgid "Delete Account" +msgstr "" + +#: templates/profile.html:22 +msgid "Link Discord Account" +msgstr "" + +#: templates/profile.html:25 +msgid "Discord Linked" +msgstr "" + +#: templates/profile.html:28 +msgid "Unlink Discord" +msgstr "" + +#: templates/register.html:29 +msgid "Already have an account?" +msgstr "" + +#: templates/reset_password.html:2 templates/reset_password.html:7 +msgid "Reset Password" +msgstr "" + +#: templates/reset_password.html:14 +msgid "Request Reset" +msgstr "" + +#: templates/reset_requests.html:2 templates/reset_requests.html:5 +msgid "Password Reset Requests" +msgstr "" + +#: templates/reset_requests.html:6 +msgid "Delete All" +msgstr "" + +#: templates/reset_requests.html:12 templates/reset_requests.html:43 +#: templates/reset_requests.html:68 +msgid "Requested At" +msgstr "" + +#: templates/reset_requests.html:38 +msgid "Completed Requests" +msgstr "" + +#: templates/reset_requests.html:63 +msgid "Rejected Requests" +msgstr "" + +#: templates/reset_requests.html:88 +msgid "No open reset requests." +msgstr "" + +#: templates/secret.html:2 +msgid "Secret" +msgstr "" + +#: templates/setup.html:2 +msgid "Admin Setup" +msgstr "" + +#: templates/setup.html:4 +msgid "Admin Account Setup" +msgstr "" + +#: templates/setup.html:22 +msgid "Create Admin" +msgstr "" + +#: templates/shop.html:5 +msgid "Deine Reward-Punkte:" +msgstr "" + +#: templates/shop.html:19 +msgid "Bought" +msgstr "" + +#: templates/shop.html:24 +msgid "Buy" +msgstr "" + +#: templates/support.html:5 +msgid "If you have any questions or need assistance, please contact us at:" +msgstr "" + +#: templates/support.html:6 +msgid "Github:" +msgstr "" + +#: templates/support.html:11 +msgid "Wipe Server" +msgstr "" + +#: templates/support.html:17 templates/support.html:24 +msgid "Title" +msgstr "" + +#: templates/support.html:18 +msgid "Describe your issue..." +msgstr "" + +#: templates/support.html:19 +msgid "Create Ticket" +msgstr "" + +#: templates/support.html:36 templates/support_thread.html:7 +msgid "Open" +msgstr "" + +#: templates/support.html:38 templates/support_thread.html:9 +msgid "Closed" +msgstr "" + +#: templates/support.html:43 +msgid "View" +msgstr "" + +#: templates/support_thread.html:23 +msgid "Write a message..." +msgstr "" + +#: templates/support_thread.html:24 +msgid "Send" +msgstr "" + +#: templates/support_thread.html:27 +msgid "Close Ticket" +msgstr "" + +#: templates/support_thread.html:30 +msgid "This ticket is closed." +msgstr "" + +#: templates/support_thread.html:39 +msgid "Back to Support" +msgstr "" + +#: templates/users.html:4 +msgid "All Users" +msgstr "" + +#: templates/users.html:20 +msgid "Request sent" +msgstr "" + +#: templates/users.html:22 +msgid "Request received" +msgstr "" + +#: templates/users.html:24 +msgid "Friend" +msgstr "" + +#: templates/users.html:27 +msgid "Add Friend" +msgstr "" +