deicidus
2 years ago
commit
e49cfafe46
47 changed files with 15897 additions and 0 deletions
@ -0,0 +1,39 @@
|
||||
.DS_Store |
||||
node_modules |
||||
build |
||||
dist |
||||
dist-ssr |
||||
/production |
||||
/cert |
||||
*.local |
||||
|
||||
# local env files |
||||
.env |
||||
.env.local |
||||
.env.*.local |
||||
|
||||
# Log files |
||||
logs |
||||
*.log |
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
||||
pnpm-debug.log* |
||||
lerna-debug.log* |
||||
|
||||
# Editor directories and files |
||||
.idea |
||||
.vscode |
||||
.vscode/* |
||||
!.vscode/extensions.json |
||||
*.suo |
||||
*.ntvs* |
||||
*.njsproj |
||||
*.sln |
||||
*.code-workspace |
||||
configuration.js |
||||
client-configuration.js |
||||
database.sqlite3 |
||||
personal_access_token.git |
||||
.idea |
||||
*.sw? |
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE |
||||
Version 3, 19 November 2007 |
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> |
||||
Everyone is permitted to copy and distribute verbatim copies |
||||
of this license document, but changing it is not allowed. |
||||
|
||||
Preamble |
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for |
||||
software and other kinds of works, specifically designed to ensure |
||||
cooperation with the community in the case of network server software. |
||||
|
||||
The licenses for most software and other practical works are designed |
||||
to take away your freedom to share and change the works. By contrast, |
||||
our General Public Licenses are intended to guarantee your freedom to |
||||
share and change all versions of a program--to make sure it remains free |
||||
software for all its users. |
||||
|
||||
When we speak of free software, we are referring to freedom, not |
||||
price. Our General Public Licenses are designed to make sure that you |
||||
have the freedom to distribute copies of free software (and charge for |
||||
them if you wish), that you receive source code or can get it if you |
||||
want it, that you can change the software or use pieces of it in new |
||||
free programs, and that you know you can do these things. |
||||
|
||||
Developers that use our General Public Licenses protect your rights |
||||
with two steps: (1) assert copyright on the software, and (2) offer |
||||
you this License which gives you legal permission to copy, distribute |
||||
and/or modify the software. |
||||
|
||||
A secondary benefit of defending all users' freedom is that |
||||
improvements made in alternate versions of the program, if they |
||||
receive widespread use, become available for other developers to |
||||
incorporate. Many developers of free software are heartened and |
||||
encouraged by the resulting cooperation. However, in the case of |
||||
software used on network servers, this result may fail to come about. |
||||
The GNU General Public License permits making a modified version and |
||||
letting the public access it on a server without ever releasing its |
||||
source code to the public. |
||||
|
||||
The GNU Affero General Public License is designed specifically to |
||||
ensure that, in such cases, the modified source code becomes available |
||||
to the community. It requires the operator of a network server to |
||||
provide the source code of the modified version running there to the |
||||
users of that server. Therefore, public use of a modified version, on |
||||
a publicly accessible server, gives the public access to the source |
||||
code of the modified version. |
||||
|
||||
An older license, called the Affero General Public License and |
||||
published by Affero, was designed to accomplish similar goals. This is |
||||
a different license, not a version of the Affero GPL, but Affero has |
||||
released a new version of the Affero GPL which permits relicensing under |
||||
this license. |
||||
|
||||
The precise terms and conditions for copying, distribution and |
||||
modification follow. |
||||
|
||||
TERMS AND CONDITIONS |
||||
|
||||
0. Definitions. |
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License. |
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of |
||||
works, such as semiconductor masks. |
||||
|
||||
"The Program" refers to any copyrightable work licensed under this |
||||
License. Each licensee is addressed as "you". "Licensees" and |
||||
"recipients" may be individuals or organizations. |
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work |
||||
in a fashion requiring copyright permission, other than the making of an |
||||
exact copy. The resulting work is called a "modified version" of the |
||||
earlier work or a work "based on" the earlier work. |
||||
|
||||
A "covered work" means either the unmodified Program or a work based |
||||
on the Program. |
||||
|
||||
To "propagate" a work means to do anything with it that, without |
||||
permission, would make you directly or secondarily liable for |
||||
infringement under applicable copyright law, except executing it on a |
||||
computer or modifying a private copy. Propagation includes copying, |
||||
distribution (with or without modification), making available to the |
||||
public, and in some countries other activities as well. |
||||
|
||||
To "convey" a work means any kind of propagation that enables other |
||||
parties to make or receive copies. Mere interaction with a user through |
||||
a computer network, with no transfer of a copy, is not conveying. |
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices" |
||||
to the extent that it includes a convenient and prominently visible |
||||
feature that (1) displays an appropriate copyright notice, and (2) |
||||
tells the user that there is no warranty for the work (except to the |
||||
extent that warranties are provided), that licensees may convey the |
||||
work under this License, and how to view a copy of this License. If |
||||
the interface presents a list of user commands or options, such as a |
||||
menu, a prominent item in the list meets this criterion. |
||||
|
||||
1. Source Code. |
||||
|
||||
The "source code" for a work means the preferred form of the work |
||||
for making modifications to it. "Object code" means any non-source |
||||
form of a work. |
||||
|
||||
A "Standard Interface" means an interface that either is an official |
||||
standard defined by a recognized standards body, or, in the case of |
||||
interfaces specified for a particular programming language, one that |
||||
is widely used among developers working in that language. |
||||
|
||||
The "System Libraries" of an executable work include anything, other |
||||
than the work as a whole, that (a) is included in the normal form of |
||||
packaging a Major Component, but which is not part of that Major |
||||
Component, and (b) serves only to enable use of the work with that |
||||
Major Component, or to implement a Standard Interface for which an |
||||
implementation is available to the public in source code form. A |
||||
"Major Component", in this context, means a major essential component |
||||
(kernel, window system, and so on) of the specific operating system |
||||
(if any) on which the executable work runs, or a compiler used to |
||||
produce the work, or an object code interpreter used to run it. |
||||
|
||||
The "Corresponding Source" for a work in object code form means all |
||||
the source code needed to generate, install, and (for an executable |
||||
work) run the object code and to modify the work, including scripts to |
||||
control those activities. However, it does not include the work's |
||||
System Libraries, or general-purpose tools or generally available free |
||||
programs which are used unmodified in performing those activities but |
||||
which are not part of the work. For example, Corresponding Source |
||||
includes interface definition files associated with source files for |
||||
the work, and the source code for shared libraries and dynamically |
||||
linked subprograms that the work is specifically designed to require, |
||||
such as by intimate data communication or control flow between those |
||||
subprograms and other parts of the work. |
||||
|
||||
The Corresponding Source need not include anything that users |
||||
can regenerate automatically from other parts of the Corresponding |
||||
Source. |
||||
|
||||
The Corresponding Source for a work in source code form is that |
||||
same work. |
||||
|
||||
2. Basic Permissions. |
||||
|
||||
All rights granted under this License are granted for the term of |
||||
copyright on the Program, and are irrevocable provided the stated |
||||
conditions are met. This License explicitly affirms your unlimited |
||||
permission to run the unmodified Program. The output from running a |
||||
covered work is covered by this License only if the output, given its |
||||
content, constitutes a covered work. This License acknowledges your |
||||
rights of fair use or other equivalent, as provided by copyright law. |
||||
|
||||
You may make, run and propagate covered works that you do not |
||||
convey, without conditions so long as your license otherwise remains |
||||
in force. You may convey covered works to others for the sole purpose |
||||
of having them make modifications exclusively for you, or provide you |
||||
with facilities for running those works, provided that you comply with |
||||
the terms of this License in conveying all material for which you do |
||||
not control copyright. Those thus making or running the covered works |
||||
for you must do so exclusively on your behalf, under your direction |
||||
and control, on terms that prohibit them from making any copies of |
||||
your copyrighted material outside their relationship with you. |
||||
|
||||
Conveying under any other circumstances is permitted solely under |
||||
the conditions stated below. Sublicensing is not allowed; section 10 |
||||
makes it unnecessary. |
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law. |
||||
|
||||
No covered work shall be deemed part of an effective technological |
||||
measure under any applicable law fulfilling obligations under article |
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or |
||||
similar laws prohibiting or restricting circumvention of such |
||||
measures. |
||||
|
||||
When you convey a covered work, you waive any legal power to forbid |
||||
circumvention of technological measures to the extent such circumvention |
||||
is effected by exercising rights under this License with respect to |
||||
the covered work, and you disclaim any intention to limit operation or |
||||
modification of the work as a means of enforcing, against the work's |
||||
users, your or third parties' legal rights to forbid circumvention of |
||||
technological measures. |
||||
|
||||
4. Conveying Verbatim Copies. |
||||
|
||||
You may convey verbatim copies of the Program's source code as you |
||||
receive it, in any medium, provided that you conspicuously and |
||||
appropriately publish on each copy an appropriate copyright notice; |
||||
keep intact all notices stating that this License and any |
||||
non-permissive terms added in accord with section 7 apply to the code; |
||||
keep intact all notices of the absence of any warranty; and give all |
||||
recipients a copy of this License along with the Program. |
||||
|
||||
You may charge any price or no price for each copy that you convey, |
||||
and you may offer support or warranty protection for a fee. |
||||
|
||||
5. Conveying Modified Source Versions. |
||||
|
||||
You may convey a work based on the Program, or the modifications to |
||||
produce it from the Program, in the form of source code under the |
||||
terms of section 4, provided that you also meet all of these conditions: |
||||
|
||||
a) The work must carry prominent notices stating that you modified |
||||
it, and giving a relevant date. |
||||
|
||||
b) The work must carry prominent notices stating that it is |
||||
released under this License and any conditions added under section |
||||
7. This requirement modifies the requirement in section 4 to |
||||
"keep intact all notices". |
||||
|
||||
c) You must license the entire work, as a whole, under this |
||||
License to anyone who comes into possession of a copy. This |
||||
License will therefore apply, along with any applicable section 7 |
||||
additional terms, to the whole of the work, and all its parts, |
||||
regardless of how they are packaged. This License gives no |
||||
permission to license the work in any other way, but it does not |
||||
invalidate such permission if you have separately received it. |
||||
|
||||
d) If the work has interactive user interfaces, each must display |
||||
Appropriate Legal Notices; however, if the Program has interactive |
||||
interfaces that do not display Appropriate Legal Notices, your |
||||
work need not make them do so. |
||||
|
||||
A compilation of a covered work with other separate and independent |
||||
works, which are not by their nature extensions of the covered work, |
||||
and which are not combined with it such as to form a larger program, |
||||
in or on a volume of a storage or distribution medium, is called an |
||||
"aggregate" if the compilation and its resulting copyright are not |
||||
used to limit the access or legal rights of the compilation's users |
||||
beyond what the individual works permit. Inclusion of a covered work |
||||
in an aggregate does not cause this License to apply to the other |
||||
parts of the aggregate. |
||||
|
||||
6. Conveying Non-Source Forms. |
||||
|
||||
You may convey a covered work in object code form under the terms |
||||
of sections 4 and 5, provided that you also convey the |
||||
machine-readable Corresponding Source under the terms of this License, |
||||
in one of these ways: |
||||
|
||||
a) Convey the object code in, or embodied in, a physical product |
||||
(including a physical distribution medium), accompanied by the |
||||
Corresponding Source fixed on a durable physical medium |
||||
customarily used for software interchange. |
||||
|
||||
b) Convey the object code in, or embodied in, a physical product |
||||
(including a physical distribution medium), accompanied by a |
||||
written offer, valid for at least three years and valid for as |
||||
long as you offer spare parts or customer support for that product |
||||
model, to give anyone who possesses the object code either (1) a |
||||
copy of the Corresponding Source for all the software in the |
||||
product that is covered by this License, on a durable physical |
||||
medium customarily used for software interchange, for a price no |
||||
more than your reasonable cost of physically performing this |
||||
conveying of source, or (2) access to copy the |
||||
Corresponding Source from a network server at no charge. |
||||
|
||||
c) Convey individual copies of the object code with a copy of the |
||||
written offer to provide the Corresponding Source. This |
||||
alternative is allowed only occasionally and noncommercially, and |
||||
only if you received the object code with such an offer, in accord |
||||
with subsection 6b. |
||||
|
||||
d) Convey the object code by offering access from a designated |
||||
place (gratis or for a charge), and offer equivalent access to the |
||||
Corresponding Source in the same way through the same place at no |
||||
further charge. You need not require recipients to copy the |
||||
Corresponding Source along with the object code. If the place to |
||||
copy the object code is a network server, the Corresponding Source |
||||
may be on a different server (operated by you or a third party) |
||||
that supports equivalent copying facilities, provided you maintain |
||||
clear directions next to the object code saying where to find the |
||||
Corresponding Source. Regardless of what server hosts the |
||||
Corresponding Source, you remain obligated to ensure that it is |
||||
available for as long as needed to satisfy these requirements. |
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided |
||||
you inform other peers where the object code and Corresponding |
||||
Source of the work are being offered to the general public at no |
||||
charge under subsection 6d. |
||||
|
||||
A separable portion of the object code, whose source code is excluded |
||||
from the Corresponding Source as a System Library, need not be |
||||
included in conveying the object code work. |
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any |
||||
tangible personal property which is normally used for personal, family, |
||||
or household purposes, or (2) anything designed or sold for incorporation |
||||
into a dwelling. In determining whether a product is a consumer product, |
||||
doubtful cases shall be resolved in favor of coverage. For a particular |
||||
product received by a particular user, "normally used" refers to a |
||||
typical or common use of that class of product, regardless of the status |
||||
of the particular user or of the way in which the particular user |
||||
actually uses, or expects or is expected to use, the product. A product |
||||
is a consumer product regardless of whether the product has substantial |
||||
commercial, industrial or non-consumer uses, unless such uses represent |
||||
the only significant mode of use of the product. |
||||
|
||||
"Installation Information" for a User Product means any methods, |
||||
procedures, authorization keys, or other information required to install |
||||
and execute modified versions of a covered work in that User Product from |
||||
a modified version of its Corresponding Source. The information must |
||||
suffice to ensure that the continued functioning of the modified object |
||||
code is in no case prevented or interfered with solely because |
||||
modification has been made. |
||||
|
||||
If you convey an object code work under this section in, or with, or |
||||
specifically for use in, a User Product, and the conveying occurs as |
||||
part of a transaction in which the right of possession and use of the |
||||
User Product is transferred to the recipient in perpetuity or for a |
||||
fixed term (regardless of how the transaction is characterized), the |
||||
Corresponding Source conveyed under this section must be accompanied |
||||
by the Installation Information. But this requirement does not apply |
||||
if neither you nor any third party retains the ability to install |
||||
modified object code on the User Product (for example, the work has |
||||
been installed in ROM). |
||||
|
||||
The requirement to provide Installation Information does not include a |
||||
requirement to continue to provide support service, warranty, or updates |
||||
for a work that has been modified or installed by the recipient, or for |
||||
the User Product in which it has been modified or installed. Access to a |
||||
network may be denied when the modification itself materially and |
||||
adversely affects the operation of the network or violates the rules and |
||||
protocols for communication across the network. |
||||
|
||||
Corresponding Source conveyed, and Installation Information provided, |
||||
in accord with this section must be in a format that is publicly |
||||
documented (and with an implementation available to the public in |
||||
source code form), and must require no special password or key for |
||||
unpacking, reading or copying. |
||||
|
||||
7. Additional Terms. |
||||
|
||||
"Additional permissions" are terms that supplement the terms of this |
||||
License by making exceptions from one or more of its conditions. |
||||
Additional permissions that are applicable to the entire Program shall |
||||
be treated as though they were included in this License, to the extent |
||||
that they are valid under applicable law. If additional permissions |
||||
apply only to part of the Program, that part may be used separately |
||||
under those permissions, but the entire Program remains governed by |
||||
this License without regard to the additional permissions. |
||||
|
||||
When you convey a copy of a covered work, you may at your option |
||||
remove any additional permissions from that copy, or from any part of |
||||
it. (Additional permissions may be written to require their own |
||||
removal in certain cases when you modify the work.) You may place |
||||
additional permissions on material, added by you to a covered work, |
||||
for which you have or can give appropriate copyright permission. |
||||
|
||||
Notwithstanding any other provision of this License, for material you |
||||
add to a covered work, you may (if authorized by the copyright holders of |
||||
that material) supplement the terms of this License with terms: |
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the |
||||
terms of sections 15 and 16 of this License; or |
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or |
||||
author attributions in that material or in the Appropriate Legal |
||||
Notices displayed by works containing it; or |
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or |
||||
requiring that modified versions of such material be marked in |
||||
reasonable ways as different from the original version; or |
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or |
||||
authors of the material; or |
||||
|
||||
e) Declining to grant rights under trademark law for use of some |
||||
trade names, trademarks, or service marks; or |
||||
|
||||
f) Requiring indemnification of licensors and authors of that |
||||
material by anyone who conveys the material (or modified versions of |
||||
it) with contractual assumptions of liability to the recipient, for |
||||
any liability that these contractual assumptions directly impose on |
||||
those licensors and authors. |
||||
|
||||
All other non-permissive additional terms are considered "further |
||||
restrictions" within the meaning of section 10. If the Program as you |
||||
received it, or any part of it, contains a notice stating that it is |
||||
governed by this License along with a term that is a further |
||||
restriction, you may remove that term. If a license document contains |
||||
a further restriction but permits relicensing or conveying under this |
||||
License, you may add to a covered work material governed by the terms |
||||
of that license document, provided that the further restriction does |
||||
not survive such relicensing or conveying. |
||||
|
||||
If you add terms to a covered work in accord with this section, you |
||||
must place, in the relevant source files, a statement of the |
||||
additional terms that apply to those files, or a notice indicating |
||||
where to find the applicable terms. |
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the |
||||
form of a separately written license, or stated as exceptions; |
||||
the above requirements apply either way. |
||||
|
||||
8. Termination. |
||||
|
||||
You may not propagate or modify a covered work except as expressly |
||||
provided under this License. Any attempt otherwise to propagate or |
||||
modify it is void, and will automatically terminate your rights under |
||||
this License (including any patent licenses granted under the third |
||||
paragraph of section 11). |
||||
|
||||
However, if you cease all violation of this License, then your |
||||
license from a particular copyright holder is reinstated (a) |
||||
provisionally, unless and until the copyright holder explicitly and |
||||
finally terminates your license, and (b) permanently, if the copyright |
||||
holder fails to notify you of the violation by some reasonable means |
||||
prior to 60 days after the cessation. |
||||
|
||||
Moreover, your license from a particular copyright holder is |
||||
reinstated permanently if the copyright holder notifies you of the |
||||
violation by some reasonable means, this is the first time you have |
||||
received notice of violation of this License (for any work) from that |
||||
copyright holder, and you cure the violation prior to 30 days after |
||||
your receipt of the notice. |
||||
|
||||
Termination of your rights under this section does not terminate the |
||||
licenses of parties who have received copies or rights from you under |
||||
this License. If your rights have been terminated and not permanently |
||||
reinstated, you do not qualify to receive new licenses for the same |
||||
material under section 10. |
||||
|
||||
9. Acceptance Not Required for Having Copies. |
||||
|
||||
You are not required to accept this License in order to receive or |
||||
run a copy of the Program. Ancillary propagation of a covered work |
||||
occurring solely as a consequence of using peer-to-peer transmission |
||||
to receive a copy likewise does not require acceptance. However, |
||||
nothing other than this License grants you permission to propagate or |
||||
modify any covered work. These actions infringe copyright if you do |
||||
not accept this License. Therefore, by modifying or propagating a |
||||
covered work, you indicate your acceptance of this License to do so. |
||||
|
||||
10. Automatic Licensing of Downstream Recipients. |
||||
|
||||
Each time you convey a covered work, the recipient automatically |
||||
receives a license from the original licensors, to run, modify and |
||||
propagate that work, subject to this License. You are not responsible |
||||
for enforcing compliance by third parties with this License. |
||||
|
||||
An "entity transaction" is a transaction transferring control of an |
||||
organization, or substantially all assets of one, or subdividing an |
||||
organization, or merging organizations. If propagation of a covered |
||||
work results from an entity transaction, each party to that |
||||
transaction who receives a copy of the work also receives whatever |
||||
licenses to the work the party's predecessor in interest had or could |
||||
give under the previous paragraph, plus a right to possession of the |
||||
Corresponding Source of the work from the predecessor in interest, if |
||||
the predecessor has it or can get it with reasonable efforts. |
||||
|
||||
You may not impose any further restrictions on the exercise of the |
||||
rights granted or affirmed under this License. For example, you may |
||||
not impose a license fee, royalty, or other charge for exercise of |
||||
rights granted under this License, and you may not initiate litigation |
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that |
||||
any patent claim is infringed by making, using, selling, offering for |
||||
sale, or importing the Program or any portion of it. |
||||
|
||||
11. Patents. |
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this |
||||
License of the Program or a work on which the Program is based. The |
||||
work thus licensed is called the contributor's "contributor version". |
||||
|
||||
A contributor's "essential patent claims" are all patent claims |
||||
owned or controlled by the contributor, whether already acquired or |
||||
hereafter acquired, that would be infringed by some manner, permitted |
||||
by this License, of making, using, or selling its contributor version, |
||||
but do not include claims that would be infringed only as a |
||||
consequence of further modification of the contributor version. For |
||||
purposes of this definition, "control" includes the right to grant |
||||
patent sublicenses in a manner consistent with the requirements of |
||||
this License. |
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free |
||||
patent license under the contributor's essential patent claims, to |
||||
make, use, sell, offer for sale, import and otherwise run, modify and |
||||
propagate the contents of its contributor version. |
||||
|
||||
In the following three paragraphs, a "patent license" is any express |
||||
agreement or commitment, however denominated, not to enforce a patent |
||||
(such as an express permission to practice a patent or covenant not to |
||||
sue for patent infringement). To "grant" such a patent license to a |
||||
party means to make such an agreement or commitment not to enforce a |
||||
patent against the party. |
||||
|
||||
If you convey a covered work, knowingly relying on a patent license, |
||||
and the Corresponding Source of the work is not available for anyone |
||||
to copy, free of charge and under the terms of this License, through a |
||||
publicly available network server or other readily accessible means, |
||||
then you must either (1) cause the Corresponding Source to be so |
||||
available, or (2) arrange to deprive yourself of the benefit of the |
||||
patent license for this particular work, or (3) arrange, in a manner |
||||
consistent with the requirements of this License, to extend the patent |
||||
license to downstream recipients. "Knowingly relying" means you have |
||||
actual knowledge that, but for the patent license, your conveying the |
||||
covered work in a country, or your recipient's use of the covered work |
||||
in a country, would infringe one or more identifiable patents in that |
||||
country that you have reason to believe are valid. |
||||
|
||||
If, pursuant to or in connection with a single transaction or |
||||
arrangement, you convey, or propagate by procuring conveyance of, a |
||||
covered work, and grant a patent license to some of the parties |
||||
receiving the covered work authorizing them to use, propagate, modify |
||||
or convey a specific copy of the covered work, then the patent license |
||||
you grant is automatically extended to all recipients of the covered |
||||
work and works based on it. |
||||
|
||||
A patent license is "discriminatory" if it does not include within |
||||
the scope of its coverage, prohibits the exercise of, or is |
||||
conditioned on the non-exercise of one or more of the rights that are |
||||
specifically granted under this License. You may not convey a covered |
||||
work if you are a party to an arrangement with a third party that is |
||||
in the business of distributing software, under which you make payment |
||||
to the third party based on the extent of your activity of conveying |
||||
the work, and under which the third party grants, to any of the |
||||
parties who would receive the covered work from you, a discriminatory |
||||
patent license (a) in connection with copies of the covered work |
||||
conveyed by you (or copies made from those copies), or (b) primarily |
||||
for and in connection with specific products or compilations that |
||||
contain the covered work, unless you entered into that arrangement, |
||||
or that patent license was granted, prior to 28 March 2007. |
||||
|
||||
Nothing in this License shall be construed as excluding or limiting |
||||
any implied license or other defenses to infringement that may |
||||
otherwise be available to you under applicable patent law. |
||||
|
||||
12. No Surrender of Others' Freedom. |
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or |
||||
otherwise) that contradict the conditions of this License, they do not |
||||
excuse you from the conditions of this License. If you cannot convey a |
||||
covered work so as to satisfy simultaneously your obligations under this |
||||
License and any other pertinent obligations, then as a consequence you may |
||||
not convey it at all. For example, if you agree to terms that obligate you |
||||
to collect a royalty for further conveying from those to whom you convey |
||||
the Program, the only way you could satisfy both those terms and this |
||||
License would be to refrain entirely from conveying the Program. |
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License. |
||||
|
||||
Notwithstanding any other provision of this License, if you modify the |
||||
Program, your modified version must prominently offer all users |
||||
interacting with it remotely through a computer network (if your version |
||||
supports such interaction) an opportunity to receive the Corresponding |
||||
Source of your version by providing access to the Corresponding Source |
||||
from a network server at no charge, through some standard or customary |
||||
means of facilitating copying of software. This Corresponding Source |
||||
shall include the Corresponding Source for any work covered by version 3 |
||||
of the GNU General Public License that is incorporated pursuant to the |
||||
following paragraph. |
||||
|
||||
Notwithstanding any other provision of this License, you have |
||||
permission to link or combine any covered work with a work licensed |
||||
under version 3 of the GNU General Public License into a single |
||||
combined work, and to convey the resulting work. The terms of this |
||||
License will continue to apply to the part which is the covered work, |
||||
but the work with which it is combined will remain governed by version |
||||
3 of the GNU General Public License. |
||||
|
||||
14. Revised Versions of this License. |
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of |
||||
the GNU Affero General Public License from time to time. Such new versions |
||||
will be similar in spirit to the present version, but may differ in detail to |
||||
address new problems or concerns. |
||||
|
||||
Each version is given a distinguishing version number. If the |
||||
Program specifies that a certain numbered version of the GNU Affero General |
||||
Public License "or any later version" applies to it, you have the |
||||
option of following the terms and conditions either of that numbered |
||||
version or of any later version published by the Free Software |
||||
Foundation. If the Program does not specify a version number of the |
||||
GNU Affero General Public License, you may choose any version ever published |
||||
by the Free Software Foundation. |
||||
|
||||
If the Program specifies that a proxy can decide which future |
||||
versions of the GNU Affero General Public License can be used, that proxy's |
||||
public statement of acceptance of a version permanently authorizes you |
||||
to choose that version for the Program. |
||||
|
||||
Later license versions may give you additional or different |
||||
permissions. However, no additional obligations are imposed on any |
||||
author or copyright holder as a result of your choosing to follow a |
||||
later version. |
||||
|
||||
15. Disclaimer of Warranty. |
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY |
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT |
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY |
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, |
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM |
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF |
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION. |
||||
|
||||
16. Limitation of Liability. |
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS |
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY |
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE |
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF |
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD |
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), |
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF |
||||
SUCH DAMAGES. |
||||
|
||||
17. Interpretation of Sections 15 and 16. |
||||
|
||||
If the disclaimer of warranty and limitation of liability provided |
||||
above cannot be given local legal effect according to their terms, |
||||
reviewing courts shall apply local law that most closely approximates |
||||
an absolute waiver of all civil liability in connection with the |
||||
Program, unless a warranty or assumption of liability accompanies a |
||||
copy of the Program in return for a fee. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
How to Apply These Terms to Your New Programs |
||||
|
||||
If you develop a new program, and you want it to be of the greatest |
||||
possible use to the public, the best way to achieve this is to make it |
||||
free software which everyone can redistribute and change under these terms. |
||||
|
||||
To do so, attach the following notices to the program. It is safest |
||||
to attach them to the start of each source file to most effectively |
||||
state the exclusion of warranty; and each file should have at least |
||||
the "copyright" line and a pointer to where the full notice is found. |
||||
|
||||
<one line to give the program's name and a brief idea of what it does.> |
||||
Copyright (C) <year> <name of author> |
||||
|
||||
This program is free software: you can redistribute it and/or modify |
||||
it under the terms of the GNU Affero General Public License as published by |
||||
the Free Software Foundation, either version 3 of the License, or |
||||
(at your option) any later version. |
||||
|
||||
This program is distributed in the hope that it will be useful, |
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
GNU Affero General Public License for more details. |
||||
|
||||
You should have received a copy of the GNU Affero General Public License |
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. |
||||
|
||||
Also add information on how to contact you by electronic and paper mail. |
||||
|
||||
If your software can interact with users remotely through a computer |
||||
network, you should also make sure that it provides a way for users to |
||||
get its source. For example, if your program is a web application, its |
||||
interface could display a "Source" link that leads users to an archive |
||||
of the code. There are many ways you could offer source, and different |
||||
solutions will be better for different programs; see section 13 for the |
||||
specific requirements. |
||||
|
||||
You should also get your employer (if you work as a programmer) or school, |
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. |
||||
For more information on this, and how to apply and follow the GNU AGPL, see |
||||
<https://www.gnu.org/licenses/>. |
@ -0,0 +1,4 @@
|
||||
# Top cleanup in this codebase |
||||
- Replace configuration.js with .env file (loaded automatically by Vite) |
||||
- Figure out why the client can't contact the server |
||||
- Fix 801 TypeScript errors on server |
@ -0,0 +1,44 @@
|
||||
{ |
||||
"name": "ao-server", |
||||
"version": "0.0.1", |
||||
"repository": "http://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-server.git", |
||||
"license": "AGPL-3.0-or-later", |
||||
"scripts": { |
||||
"build": "npx tsc --project tsconfig.json", |
||||
"server": "npm run build && node dist/src/server/app.js" |
||||
}, |
||||
"type": "module", |
||||
"dependencies": { |
||||
"better-sqlite3": "^7.5.1", |
||||
"bitcoin-core": "^3.0.0", |
||||
"buffer": "^6.0.3", |
||||
"cookie-parser": "^1.4.6", |
||||
"cron": "^2.0.0", |
||||
"date-fns": "^2.28.0", |
||||
"express": "^4.18.1", |
||||
"kefir": "^3.8.8", |
||||
"lodash.samplesize": "^4.2.0", |
||||
"multer": "^1.4.4", |
||||
"rsync": "^0.6.1", |
||||
"sha.js": "^2.4.11", |
||||
"socket.io": "^4.5.0", |
||||
"socket.io-client": "^4.5.0", |
||||
"socketio-auth": "^0.1.1", |
||||
"superagent": "^7.1.3", |
||||
"tor-request": "^3.1.0", |
||||
"uuid": "^8.3.2", |
||||
"youtube-dl-wrap": "^2.1.1" |
||||
}, |
||||
"devDependencies": { |
||||
"@types/socket.io": "^3.0.2", |
||||
"@types/superagent": "^4.1.15", |
||||
"chalk": "^5.0.1", |
||||
"dotenv": "^16.0.1", |
||||
"hash.js": "^1.1.7", |
||||
"ts-node": "^10.8.1", |
||||
"tsconfig-paths": "^4.0.0", |
||||
"typescript": "^4.8.0-dev.20220613", |
||||
"vite": "^2.9.9", |
||||
"vite-plugin-node": "^1.0.0" |
||||
} |
||||
} |
@ -0,0 +1,122 @@
|
||||
const satsPerBtc = 100000000 // one hundred million per btc
|
||||
import { createHash } from './crypto.js' |
||||
import { Task } from './types.js' |
||||
|
||||
export function crawlerHash(tasks: Task[], taskId: string) { |
||||
const crawlerResults = crawler(tasks, taskId) |
||||
let buffers: number[] = [] |
||||
crawlerResults.forEach(tId => { |
||||
buffers.push(...Buffer.from(tId)) |
||||
}) |
||||
const bufferResult = Buffer.from(buffers) |
||||
return createHash(bufferResult) |
||||
} |
||||
|
||||
export function crawler(tasks: Task[], taskId: string): string[] { |
||||
let history: string[] = [] |
||||
tasks.forEach(task => { |
||||
if (task.taskId === taskId) { |
||||
let crawler: string[] = [taskId] |
||||
do { |
||||
let newCards: string[] = [] |
||||
crawler.forEach(t => { |
||||
if (history.indexOf(t) >= 0) return |
||||
history.push(t) |
||||
let subTask = tasks.filter(pst => pst.taskId === t)[0] |
||||
if (subTask) { |
||||
let gridCells = |
||||
subTask.pins && subTask.pins.length >= 1 |
||||
? subTask.pins.map(pin => pin.taskId) |
||||
: [] |
||||
newCards = newCards |
||||
.concat(subTask.subTasks) |
||||
.concat(subTask.priorities) |
||||
.concat(subTask.completed) |
||||
.concat(gridCells) |
||||
} |
||||
}) |
||||
crawler = newCards |
||||
} while (crawler.length > 0) |
||||
} |
||||
}) |
||||
return history |
||||
} |
||||
|
||||
export function shortName(name) { |
||||
let limit = 280 |
||||
let shortened = name.substring(0, limit) |
||||
if (name.length > limit) { |
||||
shortened += '…' |
||||
} |
||||
return shortened |
||||
} |
||||
|
||||
export function cardColorCSS(color) { |
||||
return { |
||||
redwx: color == 'red', |
||||
bluewx: color == 'blue', |
||||
greenwx: color == 'green', |
||||
yellowwx: color == 'yellow', |
||||
purplewx: color == 'purple', |
||||
blackwx: color == 'black', |
||||
} |
||||
} |
||||
|
||||
export function isString(x) { |
||||
return Object.prototype.toString.call(x) === '[object String]' |
||||
} |
||||
|
||||
export function cadToSats(cadAmt, spot) { |
||||
let sats = (parseFloat(cadAmt) / parseFloat(spot)) * satsPerBtc |
||||
return Math.floor(sats) |
||||
} |
||||
|
||||
export function satsToCad(sats, spot) { |
||||
let cad = sats * (spot / satsPerBtc) |
||||
return cad.toFixed(2) |
||||
} |
||||
|
||||
export function calculateMsThisMonth() { |
||||
let today = new Date() |
||||
let daysThisMonth = new Date( |
||||
today.getFullYear(), |
||||
today.getMonth(), |
||||
0 |
||||
).getDate() |
||||
return daysThisMonth * 24 * 60 * 60 * 1000 |
||||
} |
||||
|
||||
export function getMeridienTime(ts) { |
||||
let d = new Date(parseInt(ts)) |
||||
let hour24 = d.getHours() |
||||
|
||||
let rollover = 0 |
||||
if (hour24 >= 24) { |
||||
rollover = 1 |
||||
hour24 %= 24 |
||||
} |
||||
|
||||
let hour, meridien |
||||
if (hour24 > 12) { |
||||
meridien = 'pm' |
||||
hour = hour24 - 12 |
||||
} else { |
||||
meridien = 'am' |
||||
hour = hour24 |
||||
} |
||||
|
||||
let date = d.getDate() + rollover |
||||
let month = d.getMonth() + 1 |
||||
let minute = d.getMinutes() |
||||
let year = d.getFullYear() |
||||
|
||||
let weekday = d.toString().slice(0, 3) |
||||
|
||||
return { weekday, year, month, date, hour, minute, meridien } |
||||
} |
||||
|
||||
export function toTitleCase(str) { |
||||
return str.replace(/\w\S*/g, function (txt) { |
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() |
||||
}) |
||||
} |
@ -0,0 +1,798 @@
|
||||
import { isString } from './calculations.js' |
||||
import { v1 } from 'uuid' |
||||
import { |
||||
Task, |
||||
Signature, |
||||
UserSeen, |
||||
CardZone, |
||||
Coords, |
||||
CardLocation, |
||||
Pinboard, |
||||
PinboardStyle, |
||||
CardPass, |
||||
} from './types.js' |
||||
|
||||
// With this set to 1, actions will occur immediately, as if they were not potential-based actions
|
||||
export const POTENTIALS_TO_EXECUTE = 1 |
||||
|
||||
// Default style when a new pinboard is created without specifiying the style
|
||||
const defaultPinboardStyle: PinboardStyle = 'pyramid' |
||||
|
||||
// Grid squares have a width measure in ems. The default number for this is 9 and the +/- buttons increase or decrease this by 1.
|
||||
const defaultSquareSizeInEms = 9 |
||||
|
||||
// The one and only function to create a new blank card, please use it everywhere cards are created for standardization and searchability.
|
||||
export function blankCard( |
||||
taskId, |
||||
name, |
||||
color, |
||||
created, |
||||
deck = [], |
||||
parents = [], |
||||
height = undefined, |
||||
width = undefined |
||||
): Task { |
||||
let newCard = { |
||||
taskId, |
||||
color, |
||||
deck, |
||||
name: typeof name !== 'string' ? 'invalid filename' : name.trim(), |
||||
address: '', // Could be left undefined by default?
|
||||
bolt11: '', // Could be left undefined by default?
|
||||
book: undefined, // Could be left undefined by default?
|
||||
boost: 0, |
||||
priorities: [], |
||||
subTasks: [], |
||||
completed: [], |
||||
pinboard: |
||||
height && width && height >= 1 && width >= 1 |
||||
? blankPinboard(height, width) |
||||
: null, |
||||
pins: [], |
||||
parents: parents, |
||||
claimed: [], |
||||
passed: [], |
||||
signed: [], // Could be left undefined by default?
|
||||
guild: false, |
||||
created: created, |
||||
lastClaimed: 0, |
||||
payment_hash: '', // Could be left undefined by default?
|
||||
highlights: [], |
||||
seen: deck.length >= 1 ? [{ memberId: deck[0], timestamp: created }] : [], |
||||
time: [], |
||||
allocations: [], |
||||
} |
||||
return newCard |
||||
} |
||||
|
||||
// Returns a blank pinboard of the default style
|
||||
export function blankPinboard( |
||||
height = 3, |
||||
width = 3, |
||||
spread = defaultPinboardStyle |
||||
): Pinboard { |
||||
const newGrid = { |
||||
spread: spread, |
||||
height: height, |
||||
width: width, |
||||
size: defaultSquareSizeInEms, |
||||
} |
||||
return newGrid |
||||
} |
||||
|
||||
// Version of this function for the server, merge with above
|
||||
export function allReachableHeldParentsServer(tasks, origin, memberId) { |
||||
if (!origin?.hasOwnProperty('taskId')) { |
||||
return [] |
||||
} |
||||
let queue = [origin] |
||||
let reachableCards = [] |
||||
|
||||
let visited = {} |
||||
visited[origin.taskId] = true |
||||
let i = 0 |
||||
while (queue.length >= 1) { |
||||
let task = queue.pop() |
||||
if ( |
||||
task === undefined || |
||||
task.subTasks === undefined || |
||||
task.priorities === undefined || |
||||
task.completed === undefined |
||||
) { |
||||
console.log('Invalid task found during returned cards search, skipping.') |
||||
continue |
||||
} |
||||
|
||||
if (task.deck.indexOf(memberId) < 0 && task.taskId !== memberId) { |
||||
continue |
||||
} |
||||
|
||||
reachableCards.push(task) |
||||
if (task.hasOwnProperty('parents') && task.parents.length >= 1) { |
||||
let parents = tasks.filter(taskItem => |
||||
task.parents.includes(taskItem.taskId) |
||||
) |
||||
parents.forEach(st => { |
||||
if (!st.hasOwnProperty('taskId')) { |
||||
console.log('Missing parent found during returned cards search.') |
||||
return |
||||
} |
||||
if (!visited.hasOwnProperty(st.taskId)) { |
||||
visited[st.taskId] = true |
||||
queue.push(st) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
return reachableCards |
||||
} |
||||
|
||||
let dupesGlossary = {} |
||||
// Adds a synonym taskId for another taskId to the duplicates glossary
|
||||
export function registerDuplicateTaskId(originalTaskId, duplicateTaskId) { |
||||
if (!dupesGlossary[duplicateTaskId]) { |
||||
dupesGlossary[duplicateTaskId] = originalTaskId |
||||
console.log(Object.keys(dupesGlossary).length, 'cards with duplicates') |
||||
} |
||||
} |
||||
|
||||
// Returns the task with the given taskId from the given list of tasks, or null
|
||||
// Uses the duplicates glossary to return synonymous tasks that were created by junk task-created events
|
||||
export function getTask(tasks, taskId) { |
||||
// Look up duplicate tasks in the duplicates glossary to politely overlook duplicate task-created mutations
|
||||
let loops = 0 |
||||
while (dupesGlossary[taskId] && loops < 4) { |
||||
taskId = dupesGlossary[taskId] |
||||
//console.log("Looked up duplicate task:", taskId, " (", Object.keys(dupesGlossary).length, " duplicated)")
|
||||
loops++ |
||||
} |
||||
if (loops >= 4) { |
||||
console.log( |
||||
'Woah, four or more redirects in the duplicates glossary, something weird' |
||||
) |
||||
} |
||||
return tasks.find(task => { |
||||
if (task.taskId === taskId) { |
||||
return task |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// Returns the first task that exactly matches the given value of the given property
|
||||
// Todo: move property to be the first argument
|
||||
export function getTaskBy(tasks, value, property) { |
||||
return tasks.find(task => { |
||||
if (task[property] === value) { |
||||
return task |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// Returns true if the given taskId matches an existing task/card in the state/database
|
||||
// The state should match the database exactly,
|
||||
// because it is sourced from the database via the deterministic event mutations in mutations.js
|
||||
export function taskExists(tasks, taskId) { |
||||
return tasks.some(task => task.taskId === taskId) |
||||
} |
||||
|
||||
// Marks the task as seen by the given memberId
|
||||
export function seeTask(task, memberId) { |
||||
if (!task.seen) { |
||||
task.seen = [] |
||||
} |
||||
task.seen = task.seen.filter(seenObject => seenObject.memberId !== memberId) |
||||
task.seen.push({ memberId: memberId, timestamp: Date.now() }) |
||||
} |
||||
|
||||
// Clears any pending passes to the specified memberId from the task
|
||||
// This is done whenever a member accepts a pass or grabs or handles a card
|
||||
export function clearPassesTo(tasks, task, memberId, alsoClearFrom = false) { |
||||
const lengthBefore = task.passed.length |
||||
task.passed = task.passed.filter( |
||||
d => d[1] !== memberId || (alsoClearFrom ? d[0] !== memberId : false) |
||||
) |
||||
if (lengthBefore != task.passed.length) { |
||||
const memberCard = getTask(tasks, memberId) |
||||
changeGiftCount(memberCard, task.passed.length - lengthBefore) |
||||
} |
||||
} |
||||
|
||||
// Takes a member card and increases or decreases its .giftCount property, adding it if necessary
|
||||
export function changeGiftCount(memberCard, amount) { |
||||
if (!memberCard) { |
||||
return |
||||
} |
||||
|
||||
if (!memberCard.hasOwnProperty('giftCount')) { |
||||
memberCard.giftCount = 0 |
||||
} |
||||
|
||||
memberCard.giftCount += amount |
||||
|
||||
if (memberCard.giftCount < 0) { |
||||
memberCard.giftCount = 0 |
||||
} |
||||
} |
||||
|
||||
// Grabs a task, adding it to the member's deck
|
||||
// The associated pending pass on the card, if any, will be removed
|
||||
// You cannot grab your own member card
|
||||
export function grabTask(tasks, task, memberId) { |
||||
clearPassesTo(tasks, task, memberId) |
||||
if (memberId && task.deck.indexOf(memberId) === -1) { |
||||
if (task.taskId !== memberId) { |
||||
task.deck.push(memberId) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Drops the task, removing it from the member's deck
|
||||
export function dropTask(task, memberId) { |
||||
task.deck = task.deck.filter(d => d !== memberId) |
||||
} |
||||
|
||||
// Adds the given taskId to list of the card's parents (this is a cache)
|
||||
export function addParent(task, parentId) { |
||||
if (!task.hasOwnProperty('parents') || !Array.isArray(task.parents)) { |
||||
task.parents = [] |
||||
} |
||||
if (!task.parents.some(pId => pId === parentId)) { |
||||
task.parents.push(parentId) |
||||
} |
||||
} |
||||
|
||||
// Removes the given taskId from the list of the card's parents
|
||||
// This function seems to make no sense
|
||||
export function removeParent(task, parentId) { |
||||
if (!task.hasOwnProperty('parents') || !Array.isArray(task.parents)) { |
||||
return |
||||
} |
||||
let gridCells = [] |
||||
if (task.pins && task.pins.length >= 1) { |
||||
gridCells = task.pins.map(pin => pin.taskId) |
||||
} |
||||
|
||||
let stashItems: string[] = [] |
||||
if (task.stash && Object.keys(task.stash).length >= 1) { |
||||
stashItems = [...Object.values<string>(task.stash)] |
||||
} |
||||
|
||||
const allSubTasks = [ |
||||
...task.priorities, |
||||
...task.subTasks, |
||||
...gridCells, |
||||
...task.completed, |
||||
...stashItems, |
||||
] |
||||
|
||||
if (!allSubTasks.some(stId => stId === parentId)) { |
||||
task.parents = task.parents.filter(tId => tId !== parentId) |
||||
} |
||||
} |
||||
|
||||
// Removes the second card from the first card's list of parents,
|
||||
// unless the card is actuall still a parent
|
||||
export function removeParentIfNotParent(task, parent) { |
||||
if ( |
||||
!task.hasOwnProperty('parents') || |
||||
!Array.isArray(task.parents) || |
||||
task.parents.length < 1 |
||||
) { |
||||
return |
||||
} |
||||
let gridCells = [] |
||||
if (parent.pins && parent.pins.length >= 1) { |
||||
gridCells = parent.pins.map(pin => pin.taskId) |
||||
} |
||||
|
||||
let stashItems: string[] = [] |
||||
if (parent.stash && Object.keys(parent.stash).length >= 1) { |
||||
stashItems = [...Object.values<string>(parent.stash)] |
||||
} |
||||
|
||||
const allSubTasks = [ |
||||
...parent.priorities, |
||||
...parent.subTasks, |
||||
...gridCells, |
||||
...parent.completed, |
||||
...stashItems, |
||||
] |
||||
|
||||
if (!allSubTasks.some(stId => stId === task.taskId)) { |
||||
task.parents = task.parents.filter(tId => tId !== parent.taskId) |
||||
} |
||||
} |
||||
|
||||
// Removes the given taskId from the priorities, subTasks, and completed of the given task
|
||||
// Does NOT remove the taskId from the grid
|
||||
export function filterFromSubpiles(task, taskId) { |
||||
const start = [ |
||||
task?.priorities?.length || null, |
||||
task?.subTask?.length || null, |
||||
task?.completed?.length || null, |
||||
] |
||||
discardPriority(task, taskId) |
||||
discardSubTask(task, taskId) |
||||
discardCompletedTask(task, taskId) |
||||
if ( |
||||
(start[0] !== null && start[0] - task?.priorities?.length > 0) || |
||||
(start[1] !== null && start[1] - task?.subTasks?.length > 0) || |
||||
(start[2] !== null && start[2] - task?.completed?.length > 0) |
||||
) { |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// Marks as unseen (clears seen from) the given task, unless it's on the list
|
||||
// Unseens bubble-up one level, but if you've seen the child, it doesn't affect you
|
||||
export function clearSeenExcept(task, exceptionMemberIds: UserSeen[] = []) { |
||||
if (task?.seen?.length >= 1) { |
||||
task.seen = task.seen.filter(userseen => |
||||
exceptionMemberIds.includes(userseen.memberId) |
||||
) |
||||
} |
||||
} |
||||
|
||||
// Re-adds the given taskId to the given card's subTasks (moving it to the end)
|
||||
// This will move it to the top/front of the list of cards in the GUI
|
||||
// Precondition: The subtask referred to by subTaskId exists (otherwise it will create a broken card link / missing reference)
|
||||
export function addSubTask(task, subTaskId) { |
||||
if (!task) { |
||||
console.log( |
||||
'Attempting to add a subtask to a missing task, this should never happen' |
||||
) |
||||
return |
||||
} |
||||
discardSubTask(task, subTaskId) |
||||
task.subTasks.push(subTaskId) |
||||
} |
||||
|
||||
// Removes the given discardTaskId from the given task's subtasks
|
||||
function discardSubTask(task, discardTaskId) { |
||||
if (!task || !discardTaskId || !task.subTasks || task.subTasks.length <= 0) |
||||
return |
||||
task.subTasks = task.subTasks.filter(stId => stId !== discardTaskId) |
||||
} |
||||
|
||||
// Removes the given discardTaskId from the given task's completed tasks list
|
||||
function discardCompletedTask(task, discardTaskId) { |
||||
if (!task || !discardTaskId || !task.completed || task.completed.length <= 0) |
||||
return |
||||
task.completed = task.completed.filter(stId => stId !== discardTaskId) |
||||
} |
||||
|
||||
// Adds a completed task to the completed list in a card or moves it to the top of the list
|
||||
function addCompletedTask(task, completedTaskId) { |
||||
discardCompletedTask(task, completedTaskId) |
||||
task.completed.push(completedTaskId) |
||||
} |
||||
|
||||
// Adds the subTask to the given new parent task and adds the parent as a parent to the new subTask
|
||||
export function putTaskInTask(subTask, inTask) { |
||||
addSubTask(inTask, subTask.taskId) |
||||
addParent(subTask, inTask.taskId) |
||||
} |
||||
|
||||
// Re-adds the given taskId to the given card's priorities (moving it to the end)
|
||||
// This will move it to the top/front of the list of cards in the GUI
|
||||
export function addPriority(task, taskId) { |
||||
discardPriority(task, taskId) |
||||
task.priorities.push(taskId) |
||||
} |
||||
|
||||
// Removes the given discardTaskId from the given task's subtasks
|
||||
function discardPriority(task, discardTaskId) { |
||||
if ( |
||||
!task || |
||||
!discardTaskId || |
||||
!task.priorities || |
||||
task.priorities.length <= 0 |
||||
) |
||||
return |
||||
task.priorities = task.priorities.filter(stId => stId !== discardTaskId) |
||||
} |
||||
|
||||
export function unpinTasksOutOfBounds(tasks, task) { |
||||
if (!tasks || !task || !task.pins || task.pins.length <= 0) { |
||||
return |
||||
} |
||||
const vertLimit = task.pinboard.spread === 'rune' ? 1 : task.pinboard.height |
||||
for (let i = task.pins.length - 1; i >= 0; i--) { |
||||
const pin = task.pins[i] |
||||
const { taskId, y, x } = pin |
||||
const horizLimit = |
||||
task.pinboard.spread === 'pyramid' ? y + 1 : task.pinboard.width |
||||
if (x >= horizLimit || y >= vertLimit) { |
||||
const theSubTask = getTask(tasks, taskId) |
||||
unpinTaskFromTask(task, { y: y, x: x }) |
||||
if (theSubTask) { |
||||
putTaskInTask(theSubTask, task) |
||||
} else { |
||||
console.log('A missing card was removed from the pinboard:', taskId) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Unpins the card from the given coordinates in a card and returns its taskId
|
||||
function unpinTaskFromTask(task, coords) { |
||||
let result |
||||
if (!task.pins || task.pins.length <= 0) { |
||||
return null |
||||
} |
||||
task.pins.some((pin, i) => { |
||||
const { pinId, y, x } = pin |
||||
if (y == coords.y && x == coords.x) { |
||||
result = task.pins.splice(i, 1)[0] |
||||
} |
||||
}) |
||||
return result |
||||
} |
||||
|
||||
// Precondition: The spec should validate whether this is a legal move based upon the current gridStyle of the card
|
||||
// In other words, this function does not check if the coordinates are off the side of the pinboard
|
||||
// Unlike the functions to add subtasks, this function does NOT attempt to filter the pinboard before adding a card,
|
||||
// so duplicates will occur unless you unpin first from the origin coords
|
||||
// However, it WILL check where the card is going to be placed, and if a card is already there, that card will drop into .subTasks
|
||||
function pinTaskToTask(task, taskId, coords) { |
||||
if (!task.hasOwnProperty('pins') || !Array.isArray(task.pins)) { |
||||
task.pins = [] |
||||
} |
||||
|
||||
// If this taskId is already at this location, do nothing
|
||||
if ( |
||||
task.pins.some( |
||||
pin => pin.taskId === taskId && pin.y === coords.y && pin.x === coords.x |
||||
) |
||||
) { |
||||
return |
||||
} |
||||
|
||||
// If there is already something pinned there, drop it into subTasks
|
||||
const previousPinnedTaskId = unpinTaskFromTask(task, coords)?.taskId |
||||
|
||||
if (previousPinnedTaskId) { |
||||
addSubTask(task, previousPinnedTaskId) |
||||
} |
||||
|
||||
task.pins.push({ taskId, y: coords.y, x: coords.x }) |
||||
} |
||||
|
||||
function putTaskInTaskZone(task, inTask, toLocation) { |
||||
switch (toLocation.zone) { |
||||
case 'priorities': |
||||
// Move the card to the .priorities
|
||||
//filterFromSubpiles(inTask, task.taskId)
|
||||
addPriority(inTask, task.taskId) |
||||
addParent(task, inTask.taskId) |
||||
break |
||||
case 'grid': |
||||
// Move the card to the .pins using coordinates in the current gridStyle, or fail if not possible
|
||||
// If there isn't a grid on this card, add a grid large enough for the new coordinates to fit on
|
||||
if (!inTask.pinboard) { |
||||
inTask.pinboard = blankPinboard( |
||||
Math.max(toLocation.coords.y, 3), |
||||
Math.max(toLocation.coords.x, 3) |
||||
) |
||||
} |
||||
pinTaskToTask(inTask, task.taskId, toLocation.coords) |
||||
addParent(task, inTask.taskId) |
||||
break |
||||
case 'completed': |
||||
// Move the card to the .completed
|
||||
addCompletedTask(inTask, task.taskId) |
||||
break |
||||
case 'discard': |
||||
// Remove the card from its inId, or save in .completed if it's
|
||||
// Could replace task-de-sub-tasked
|
||||
filterFromSubpiles(inTask, task.taskId) |
||||
break |
||||
case 'context': |
||||
case 'panel': |
||||
// These don't do anything on the server, it's a zone only on the client
|
||||
break |
||||
case 'gifts': |
||||
// Deprecated?
|
||||
break |
||||
case 'stash': |
||||
// the .level on the toLocation.level tells what stash level to put the card in
|
||||
// Rethink this
|
||||
break |
||||
case 'card': |
||||
case 'subTasks': |
||||
default: |
||||
// Move the card to the .subTasks (replaces task-sub-tasked)
|
||||
putTaskInTask(task, inTask) |
||||
break |
||||
} |
||||
} |
||||
|
||||
// Removes the specified discardTaskId from the specified zone of the given task
|
||||
// If zone argument is 'card' or empty, tries to discard from priorities, subTasks, and completed (but not grid)
|
||||
export function discardTaskFromZone(task, fromLocation) { |
||||
switch (fromLocation.zone) { |
||||
case 'grid': |
||||
unpinTaskFromTask(task, fromLocation.coords) |
||||
return |
||||
case 'priorities': |
||||
discardPriority(task, fromLocation.taskId) |
||||
return |
||||
case 'completed': |
||||
discardCompletedTask(task, fromLocation.taskId) |
||||
return |
||||
case 'subTasks': |
||||
discardSubTask(task, fromLocation.taskId) |
||||
return |
||||
} |
||||
} |
||||
|
||||
// Moves a card from one location to another location.
|
||||
// fromLocation defines the card to be unplayed from somewhere, and toLocation defines a card to be placed somewhere.
|
||||
// fromLocation and toLocation are CardLocation objects defining a taskId in a location.
|
||||
// The fromLocation is an optional CardLocation that, if provided, requires a taskId and zone at minimum
|
||||
// If null, no card will be unplayed.
|
||||
// fromLocation.taskId and toLocation.taskId can be different,
|
||||
// so it is possible to play a different card than was unplayed in one move (i.e., swap out a card)
|
||||
// Right now the card being played must exist; card creation and modification is separate since it includes color etc.
|
||||
// Maybe toLocation should be option also, simplifying discards and further decomposing a play.
|
||||
export function atomicCardPlay( |
||||
tasks, |
||||
fromLocation: CardLocation, |
||||
toLocation: CardLocation, |
||||
memberId |
||||
) { |
||||
const taskId = |
||||
fromLocation && fromLocation.taskId |
||||
? fromLocation.taskId |
||||
: toLocation.taskId |
||||
const theCard = getTask(tasks, taskId) |
||||
if (!theCard && fromLocation?.zone !== 'grid') { |
||||
return |
||||
} |
||||
|
||||
const theCardMovedTo = getTask(tasks, toLocation.inId) |
||||
if ( |
||||
!theCardMovedTo && |
||||
!['discard', 'context', 'panel'].includes(toLocation.zone) |
||||
) { |
||||
console.log( |
||||
'Attempting to move a card to a missing card, this should never happen. Missing card:', |
||||
toLocation.inId, |
||||
'and zone:', |
||||
toLocation.zone |
||||
) |
||||
return |
||||
} |
||||
|
||||
if (theCard && memberId) { |
||||
// memberId should be required, but temporarily for debugging
|
||||
// You cannot play a card without having seen it
|
||||
seeTask(theCard, memberId) |
||||
|
||||
// You cannot play a card without first grabbing it
|
||||
grabTask(tasks, theCard, memberId) |
||||
} |
||||
|
||||
// Remove the card from wherever it was moved from
|
||||
const fromInId = fromLocation?.inId |
||||
const theCardMovedFrom = getTask(tasks, fromInId) |
||||
if (theCardMovedFrom) { |
||||
discardTaskFromZone(theCardMovedFrom, fromLocation) |
||||
if (fromLocation.inId !== toLocation.inId) { |
||||
removeParentIfNotParent(theCard, theCardMovedFrom) |
||||
} |
||||
|
||||
// Save the card to the completed cards if it has at least one checkmark
|
||||
if ( |
||||
fromLocation.zone != 'completed' && |
||||
toLocation.zone === 'discard' && |
||||
theCard && |
||||
theCard.claimed && |
||||
theCard.claimed.length >= 1 |
||||
) { |
||||
addCompletedTask(theCardMovedFrom, taskId) |
||||
} |
||||
|
||||
// If removing from priorities, remove allocations that were on the card from theCardMovedFrom
|
||||
if ( |
||||
fromLocation?.zone === 'priorities' && |
||||
toLocation.zone !== 'priorities' && |
||||
theCardMovedFrom.allocations && |
||||
Array.isArray(theCardMovedFrom.allocations) |
||||
) { |
||||
theCardMovedFrom.allocations = theCardMovedFrom.allocations.filter(al => { |
||||
if (al.allocatedId === taskId) { |
||||
theCardMovedFrom.boost += al.amount |
||||
return false |
||||
} |
||||
return true |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// Move card to wherever it was moved to
|
||||
if (theCard) { |
||||
putTaskInTaskZone(theCard, theCardMovedTo, toLocation) |
||||
} |
||||
} |
||||
|
||||
// Adds the given taskId to the given card's stash of the specified level
|
||||
// Each membership level added to a card has a corresponding stash level
|
||||
export function stashTask(task, taskId, level) { |
||||
if ( |
||||
!task.hasOwnProperty('stash') || |
||||
!( |
||||
typeof task.stash === 'object' && |
||||
task.stash !== null && |
||||
!Array.isArray(task.stash) |
||||
) |
||||
) { |
||||
task.stash = {} |
||||
} |
||||
if (!task.stash.hasOwnProperty('level')) { |
||||
task.stash[level] = [] |
||||
} |
||||
task.stash[level] = task.stash[level].filter(tId => tId !== taskId) |
||||
task.stash[level].push(taskId) |
||||
} |
||||
|
||||
// Removes the given taskId from the given card's stash of the specified level
|
||||
export function unstashTask(task, taskId, level) { |
||||
if ( |
||||
!task.hasOwnProperty('stash') || |
||||
!( |
||||
typeof task.stash === 'object' && |
||||
task.stash !== null && |
||||
!Array.isArray(task.stash) |
||||
) |
||||
) { |
||||
return |
||||
} |
||||
if (!task.stash.hasOwnProperty('level')) { |
||||
return |
||||
} |
||||
task.stash[level] = task.stash[level].filter(tId => tId !== taskId) |
||||
} |
||||
|
||||
// A potentials list is a list of signatures, each signature endorsing a specific task event-type
|
||||
// When POTENTIALS_TO_EXECUTE potentials accrue for a given event-type it is executed, like an action potential
|
||||
// This allows built-in AO mutations to be voted upon by members before execution
|
||||
// Because it is a vote, duplicate potentials for the same event-type are prevented
|
||||
export function addPotential(member, signature) { |
||||
if (!member.potentials) { |
||||
member.potentials = [] |
||||
} |
||||
|
||||
member.potentials = member.potentials.filter( |
||||
pot => |
||||
!( |
||||
pot.opinion === signature.opinion && pot.memberId === signature.memberId |
||||
) |
||||
) |
||||
|
||||
member.potentials.push(signature) |
||||
} |
||||
|
||||
// Returns true if there are POTENTIALS_TO_EXECUTE or more potentials of the specified event-type on the object
|
||||
export function checkPotential(member, eventType) { |
||||
return ( |
||||
member.potentials.filter(pot => pot.opinion === eventType).length >= |
||||
POTENTIALS_TO_EXECUTE |
||||
) |
||||
} |
||||
|
||||
// Clears all potentials of the specified event-type from the given card
|
||||
export function clearPotential(member, eventType) { |
||||
member.potentials = member.potentials.filter(pot => pot.opinion !== eventType) |
||||
} |
||||
|
||||
// Sets the lastUsed property of the given object to the given timestamp
|
||||
export function updateLastUsed(member, timestamp) { |
||||
member.lastUsed = timestamp |
||||
} |
||||
|
||||
export function safeMerge(cardA, cardZ) { |
||||
if (!cardA || !cardZ) { |
||||
console.log('attempt to merge nonexistent card') |
||||
return |
||||
} |
||||
|
||||
if (!cardZ.taskId || !isString(cardZ.taskId)) { |
||||
console.log('attempt to merge card with a missing or invalid taskId') |
||||
return |
||||
} |
||||
|
||||
if (!cardZ.color) { |
||||
console.log('attempt to merge card without a color') |
||||
return |
||||
} |
||||
|
||||
if (isString(cardZ.color) && cardZ.color.trim().length >= 1) { |
||||
cardA.color = cardZ.color |
||||
} |
||||
|
||||
if (isString(cardZ.guild) && cardZ.color.trim().length >= 1) { |
||||
cardA.guild = cardZ.guild |
||||
} |
||||
|
||||
const filterNull = tasks => { |
||||
return tasks.filter(task => task !== null && task !== undefined) |
||||
} |
||||
|
||||
cardA.book = cardZ.book |
||||
cardA.address = cardZ.address |
||||
cardA.bolt11 = cardZ.bolt11 |
||||
cardA.priorities = [ |
||||
...new Set(cardA.priorities.concat(filterNull(cardZ.priorities))), |
||||
] |
||||
cardA.subTasks = [ |
||||
...new Set(cardA.subTasks.concat(filterNull(cardZ.subTasks))), |
||||
] |
||||
cardA.completed = [ |
||||
...new Set(cardA.completed.concat(filterNull(cardZ.completed))), |
||||
] |
||||
|
||||
// Replace the pinboard (maybe they could merge? or at least drop existing pins down to subTasks)
|
||||
if ( |
||||
cardZ.pinboard && |
||||
cardZ.pinboard.height >= 1 && |
||||
cardZ.pinboard.width >= 1 |
||||
) { |
||||
if (!cardA.pinboard) { |
||||
cardA.pinboard = blankPinboard() |
||||
} |
||||
cardA.pinboard.height = Math.max( |
||||
cardA.pinboard.height, |
||||
cardZ.pinboard.height |
||||
) |
||||
cardA.pinboard.width = Math.max(cardA.pinboard.width, cardZ.pinboard.width) |
||||
cardA.pinboard.spread = cardZ.pinboard.spread |
||||
if (cardZ.pins && Array.isArray(cardZ.pins)) { |
||||
cardA.pins = cardZ.pins |
||||
} |
||||
} |
||||
|
||||
cardA.passed = [...new Set([...cardA.passed, ...filterNull(cardZ.passed)])] |
||||
// Remove duplicate passes
|
||||
let passesNoDuplicates: CardPass[] = [] |
||||
cardA.passed.forEach(pass => { |
||||
if ( |
||||
!passesNoDuplicates.some( |
||||
pass2 => pass[0] === pass2[0] && pass[1] === pass2[1] |
||||
) |
||||
) { |
||||
passesNoDuplicates.push(pass) |
||||
} |
||||
}) |
||||
cardA.passed = passesNoDuplicates |
||||
|
||||
// XXX only add in merge for now
|
||||
// XXX bolt11 / address need to clearly indicate origin ao
|
||||
// XXX book should be a list?
|
||||
} |
||||
|
||||
// A card's .signed is an append-only list of all signing events.
|
||||
// This function reduces it to just each member's current opinion
|
||||
// signed is type Signature[]
|
||||
export function mostRecentSignaturesOnly(signed) { |
||||
let mostRecentSignaturesOnly = signed.filter((signature, index) => { |
||||
let lastIndex |
||||
for (let i = signed.length - 1; i >= 0; i--) { |
||||
if (signed[i].memberId === signature.memberId) { |
||||
lastIndex = i |
||||
break |
||||
} |
||||
} |
||||
return lastIndex === index |
||||
}) |
||||
return mostRecentSignaturesOnly |
||||
} |
||||
|
||||
// Signed is type Signature[]
|
||||
export function countCurrentSignatures(signed) { |
||||
return mostRecentSignaturesOnly(signed).filter( |
||||
signature => signature.opinion >= 1 |
||||
).length |
||||
} |
@ -0,0 +1,30 @@
|
||||
import crypto from 'crypto' // Does not work on client because this is a Node library, but works for below server-only functions
|
||||
// These libraries are old but they work and can be included on both server and client
|
||||
import shajs from 'sha.js' |
||||
import hmac from 'hash.js/lib/hash/hmac.js' |
||||
import sha256 from 'hash.js/lib/hash/sha/256.js' // Only works for shorter hashes, not in createHash used for hashing meme files
|
||||
|
||||
export function createHash(payload) { |
||||
return shajs('sha256').update(payload).digest('hex') |
||||
} |
||||
|
||||
export function hmacHex(data, signingKey) { |
||||
return hmac(sha256, signingKey).update(data).digest('hex') |
||||
} |
||||
|
||||
export function derivePublicKey(p) { |
||||
return crypto.createPublicKey(p).export({ |
||||
type: 'spki', |
||||
format: 'pem', |
||||
}) |
||||
} |
||||
|
||||
export function encryptToPublic(pub, info) { |
||||
return crypto.publicEncrypt(pub, new Buffer(info)).toString('hex') |
||||
} |
||||
|
||||
export function decryptFromPrivate(priv, hiddenInfo) { |
||||
return crypto |
||||
.privateDecrypt(priv, Buffer.from(hiddenInfo, 'hex')) |
||||
.toString('latin1') |
||||
} |
@ -0,0 +1,100 @@
|
||||
import { getTask } from './cards.js' |
||||
|
||||
// DUPLICATED function from client/cardActions.ts
|
||||
// Returns the number of vouches a member has. Member card must exist for each voucher. Members do not have to be active to vouch.
|
||||
// This function can be used either on the client or the server. If on the server, server state must be passed as the second arg.
|
||||
export function countVouches(memberId, state): number | null { |
||||
let card |
||||
card = getTask(state.tasks, memberId) |
||||
if (!card || !card.hasOwnProperty('deck')) return null |
||||
|
||||
let count = 0 |
||||
|
||||
const memberCards = card.deck |
||||
.map(memberId => state.members.find(m => m.memberId === memberId)) |
||||
.forEach(memberCard => { |
||||
if (memberCard !== undefined) { |
||||
count++ |
||||
} |
||||
}) |
||||
|
||||
return count |
||||
} |
||||
|
||||
// Returns true if the senpai memberId is ahead of the kohai memberId in the members list order
|
||||
export function isAheadOf(senpaiId, kohaiId, state, errRes?) { |
||||
if (errRes === undefined) { |
||||
errRes = [] |
||||
} |
||||
let senpaiRank = state.members.findIndex(m => m.memberId === senpaiId) |
||||
let kohaiRank = state.members.findIndex(m => m.memberId === kohaiId) |
||||
if (senpaiRank < kohaiRank) { |
||||
return 1 |
||||
} else if (kohaiRank < senpaiRank) { |
||||
return -1 |
||||
} |
||||
|
||||
errRes.push('member is not ahead of other member in order of member list') |
||||
return 0 |
||||
} |
||||
|
||||
// Returns true if the senpai has more attack than the kohai has defense
|
||||
// A member's defense is their number of vouches, or the highest attack score out of anyone who vouches for them (whichever is greater)
|
||||
// This method does not check if vouchers exist, therefore it depends on the mutations being perfect
|
||||
// and there not being any invalid members leftover in the .deck / vouchers list of the other member
|
||||
export function isDecidedlyMorePopularThan(senpaiId, kohaiId, state, errRes?) { |
||||
if (errRes === undefined) { |
||||
errRes = [] |
||||
} |
||||
|
||||
const senpaiCard = state.tasks.find(t => t.taskId === senpaiId) |
||||
if (!senpaiCard) { |
||||
errRes.push('invalid member detected') |
||||
return null |
||||
} |
||||
const kohaiCard = state.tasks.find(t => t.taskId === kohaiId) |
||||
if (!kohaiCard) { |
||||
errRes.push('invalid member detected') |
||||
return null |
||||
} |
||||
|
||||
const senpaiVouches = countVouches(senpaiId, state) |
||||
|
||||
let kohaiVouchCards = state.tasks.filter( |
||||
t => kohaiCard.deck.indexOf(t.taskId) >= 0 |
||||
) |
||||
|
||||
let kohaiVouches = kohaiVouchCards.length |
||||
|
||||
kohaiVouchCards.forEach(card => { |
||||
if (card.taskId !== senpaiCard.taskId) { |
||||
kohaiVouches = Math.max(kohaiVouches, countVouches(card.taskId, state)) |
||||
} |
||||
}) |
||||
if (senpaiVouches > kohaiVouches) { |
||||
return 1 |
||||
} else if (kohaiVouches > senpaiVouches) { |
||||
return -1 |
||||
} |
||||
|
||||
errRes.push('member does not have more vouches than other member') |
||||
return 0 |
||||
} |
||||
|
||||
// Returns true if the senpaiId member is both isAheadOf and isDecidedlyMorePopularThan the kohaiId member
|
||||
export function isSenpaiOf(senpaiId, kohaiId, state, errRes?) { |
||||
if (errRes === undefined) { |
||||
errRes = [] |
||||
} |
||||
|
||||
const rank = isAheadOf(senpaiId, kohaiId, state, errRes) |
||||
const vouches = isDecidedlyMorePopularThan(senpaiId, kohaiId, state, errRes) |
||||
if (rank === 1 && vouches === 1) { |
||||
return 1 |
||||
} else if (rank === -1 && vouches === -1) { |
||||
return -1 |
||||
} |
||||
|
||||
errRes.push('member is not a senpai of the other member') |
||||
return 0 |
||||
} |
@ -0,0 +1,21 @@
|
||||
import M from '../mutations.js' |
||||
|
||||
const state = [] |
||||
|
||||
const mutations = { |
||||
setCurrent(aos, current) { |
||||
aos.length = 0 |
||||
current.ao.forEach(a => { |
||||
aos.push(a) |
||||
}) |
||||
}, |
||||
applyEvent: M.aoMuts, |
||||
} |
||||
|
||||
const actions = {} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
} |
@ -0,0 +1,42 @@
|
||||
import M from '../mutations.js' |
||||
|
||||
const state = { |
||||
alias: '', |
||||
address: '', |
||||
spot: 123456, |
||||
currency: 'CAD', |
||||
rent: 0, |
||||
cap: 0, |
||||
usedTxIds: [], |
||||
outputs: [], |
||||
channels: [], |
||||
info: { x: 1 }, |
||||
pay_index: 0, |
||||
} |
||||
|
||||
const mutations = { |
||||
setCurrent(state, current) { |
||||
state.alias = current.cash.alias |
||||
state.address = current.cash.address |
||||
state.spot = current.cash.spot |
||||
state.currency = current.cash.currency |
||||
state.rent = current.cash.rent |
||||
state.cap = current.cash.cap |
||||
state.usedTxIds = current.cash.usedTxIds |
||||
state.outputs = current.cash.outputs |
||||
state.channels = current.cash.channels |
||||
state.info = current.cash.info |
||||
state.pay_index = current.cash.pay_index |
||||
}, |
||||
applyEvent: M.cashMuts, |
||||
} |
||||
|
||||
const actions = {} |
||||
const getters = {} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
getters, |
||||
} |
@ -0,0 +1,91 @@
|
||||
const payments = ['bitcoin', 'lightning'] |
||||
|
||||
const state = { |
||||
parent: [], |
||||
panel: [], |
||||
top: 0, |
||||
completed: false, |
||||
action: false, |
||||
loading: false, |
||||
} |
||||
|
||||
const mutations = { |
||||
toggleCompleted(state) { |
||||
state.completed = !state.completed |
||||
}, |
||||
setParent(state, p) { |
||||
state.parent = p |
||||
}, |
||||
setPanel(state, panel) { |
||||
state.panel = panel |
||||
}, |
||||
setTop(state, top) { |
||||
state.top = top |
||||
}, |
||||
setAction(state, a) { |
||||
state.action = a |
||||
}, |
||||
addParent(state, pId) { |
||||
state.parent = state.parent.filter(p => p !== pId) |
||||
state.parent.push(pId) |
||||
}, |
||||
goToParent(state, tId) { |
||||
let popped = false |
||||
while (popped !== tId && state.parent.length !== 0) { |
||||
popped = state.parent.pop() |
||||
} |
||||
}, |
||||
startLoading(state, dimension) { |
||||
state.loading = dimension |
||||
}, |
||||
stopLoading(state) { |
||||
state.loading = false |
||||
}, |
||||
} |
||||
|
||||
const actions = { |
||||
loaded({ commit, state, getters, dispatch }) { |
||||
commit('stopLoading') |
||||
dispatch('flashHelm', 1) |
||||
setTimeout(() => { |
||||
commit('setAction', false) |
||||
}, 333) |
||||
let isMuted = getters.member.muted |
||||
if (!isMuted) { |
||||
try { |
||||
//let flip = new Audio(require('../sounds/pageturn.wav'))
|
||||
//flip.volume = flip.volume * 0.33
|
||||
//flip.play()
|
||||
} catch (err) {} |
||||
} |
||||
}, |
||||
goIn({ commit }, pContext) { |
||||
console.log('goIn hit', pContext) |
||||
setTimeout(() => { |
||||
commit('setAction', false) |
||||
}, 333) |
||||
commit('setPanel', pContext.panel) |
||||
commit('setTop', pContext.top) |
||||
pContext.parents.forEach(p => { |
||||
commit('addParent', p) |
||||
}) |
||||
}, |
||||
goUp({ commit }, pContext) { |
||||
console.log('goUp called') |
||||
setTimeout(() => { |
||||
commit('setAction', false) |
||||
}, 333) |
||||
commit('goToParent', pContext.target) |
||||
commit('setPanel', pContext.panel) |
||||
commit('setTop', pContext.top) |
||||
}, |
||||
} |
||||
|
||||
const getters = {} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
getters, |
||||
} |
@ -0,0 +1,71 @@
|
||||
let memes = [ |
||||
'very database', |
||||
'such create', |
||||
'wow', |
||||
'much store', |
||||
'wow', |
||||
'very happen', |
||||
'much do', |
||||
] |
||||
|
||||
let colors = ['white', 'red', 'yellow', 'lime', 'aqua', 'blue', 'fuchsia'] |
||||
|
||||
function bestMeme() { |
||||
return memes[Math.floor(Math.random() * memes.length)] |
||||
} |
||||
|
||||
const state = [] |
||||
|
||||
const mutations = { |
||||
show(state, ev) { |
||||
let newBubble = { |
||||
meme: bestMeme(), |
||||
type: '', |
||||
showEvent: false, |
||||
randomX: '45%', |
||||
randomColors: [], |
||||
randomXs: [], |
||||
randomYs: [], |
||||
} |
||||
Object.assign(newBubble, ev) |
||||
newBubble.meme = bestMeme() |
||||
newBubble.showEvent = true |
||||
newBubble.randomX = (Math.random() * 91).toFixed(2) + '%' |
||||
for (let i = 0; i < 2; i++) |
||||
newBubble.randomColors.push( |
||||
colors[Math.floor(Math.random() * colors.length)] |
||||
) |
||||
for (let i = 0; i < 2; i++) |
||||
newBubble.randomXs.push(Math.floor(Math.random() * 100) + '%') |
||||
for (let i = 0; i < 2; i++) |
||||
newBubble.randomYs.push(Math.floor(Math.random() * 100) + '%') |
||||
|
||||
state.push(newBubble) |
||||
}, |
||||
hide(state) { |
||||
state[0].showEvent = false |
||||
state.shift() |
||||
}, |
||||
} |
||||
|
||||
const actions = { |
||||
displayEvent({ commit, getters }, ev) { |
||||
if ( |
||||
!getters.member.muted && |
||||
(ev.type === 'doge-barked' || ev.type === 'resource-used') |
||||
) { |
||||
commit('bark') |
||||
return |
||||
} |
||||
commit('show', ev) |
||||
setTimeout(() => { |
||||
commit('hide') |
||||
}, 3567) |
||||
}, |
||||
} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
} |
@ -0,0 +1,155 @@
|
||||
import request from 'superagent' |
||||
import io from 'socket.io-client' |
||||
const socket = io() |
||||
|
||||
var attached = false |
||||
function attachSocket(commit, dispatch) { |
||||
if (!attached) { |
||||
socket.on('unauthorized', reason => { |
||||
commit('setConnectionError', 'Unauthorized: ' + JSON.stringify(reason)) |
||||
}) |
||||
|
||||
socket.on('connect', () => { |
||||
commit('setConnected', 'connecting') |
||||
socket.emit('authentication', { |
||||
session: state.session, |
||||
token: state.token, |
||||
}) |
||||
}) |
||||
|
||||
socket.on('authenticated', () => { |
||||
commit('setConnected', 'connected') |
||||
commit('setConnectionError', '') |
||||
socket.on('eventstream', ev => { |
||||
commit('applyEvent', ev) |
||||
dispatch('displayEvent', ev) |
||||
}) |
||||
}) |
||||
socket.on('disconnect', reason => { |
||||
commit('setConnected', 'disconnected') |
||||
commit('setConnectionError', 'disconnect: ' + reason) |
||||
}) |
||||
socket.on('connect_error', error => { |
||||
commit('setConnectionError', error.message) |
||||
}) |
||||
|
||||
socket.on('error', error => { |
||||
commit('setConnectionError', error.message) |
||||
}) |
||||
|
||||
socket.on('connect_timeout', timeout => { |
||||
commit('setConnectionError', 'Timed out: ' + timeout + 'ms') |
||||
}) |
||||
|
||||
socket.on('reconnect_attempt', timeout => { |
||||
commit('setConnected', 'connecting') |
||||
commit('setConnectionError', 'reconnect attempt') |
||||
}) |
||||
|
||||
socket.on('reconnect', timeout => { |
||||
commit('setConnected', 'connected') |
||||
commit('setConnectionError', '') |
||||
}) |
||||
|
||||
socket.on('reconnect_error', error => { |
||||
commit('setConnectionError', error.message) |
||||
}) |
||||
attached = true |
||||
} |
||||
} |
||||
|
||||
const actions = { |
||||
connectSocket({ commit, dispatch }) { |
||||
attachSocket(commit, dispatch) |
||||
}, |
||||
loadCurrent({ commit, state, dispatch }) { |
||||
console.log('loadCurrent pre') |
||||
if (state.connected !== 'connected') { |
||||
socket.connect() |
||||
} |
||||
request |
||||
.post('/tasks/gg') |
||||
.set('Authorization', state.token) |
||||
.end((err, res) => { |
||||
if (err || !res.body) { |
||||
} else { |
||||
console.log('got ', res.body.length, 'tasks from tasks endpoint') |
||||
commit('applyEvent', { |
||||
type: 'tasks-received', |
||||
tasks: res.body, |
||||
}) |
||||
} |
||||
}) |
||||
console.log('loadCurrent 2') |
||||
|
||||
request |
||||
.post('/state') |
||||
.set('Authorization', state.token) |
||||
.end((err, res) => { |
||||
if (err || !res.body) { |
||||
} else { |
||||
commit('setCurrent', res.body) |
||||
res.body.sessions.forEach(s => { |
||||
if (s.session === state.session) { |
||||
commit('setPanel', [s.ownerId]) |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
console.log('loadCurrent post') |
||||
}, |
||||
makeEvent({ commit, state, getters, dispatch }, newEv) { |
||||
let startTs = Date.now() |
||||
commit('setReqStatus', 'pending') |
||||
return request |
||||
.post('/events') |
||||
.send(newEv) |
||||
.set('Authorization', state.token) |
||||
.end((err, res) => { |
||||
if (err || !res.body) { |
||||
commit('setReqStatus', 'failed', res.body) |
||||
} else { |
||||
commit('setPing', Date.now() - startTs) |
||||
commit('setReqStatus', 'ready') |
||||
} |
||||
}) |
||||
}, |
||||
} |
||||
|
||||
const state = { |
||||
token: '', |
||||
session: '', |
||||
connected: 'disconnected', |
||||
connectionError: '', |
||||
reqStatus: 'ready', |
||||
lastPing: 1, |
||||
} |
||||
|
||||
const mutations = { |
||||
setPing(loader, ping) { |
||||
loader.lastPing = ping |
||||
}, |
||||
setReqStatus(loader, status) { |
||||
loader.reqStatus = status |
||||
}, |
||||
setAuth(loader, auth) { |
||||
loader.token = auth.token |
||||
loader.session = auth.session |
||||
}, |
||||
setConnected(loader, connected) { |
||||
loader.connected = connected |
||||
}, |
||||
setConnectionError(loader, error) { |
||||
if (error === '') { |
||||
loader.connectionError = '' |
||||
return |
||||
} |
||||
loader.connectionError = error |
||||
}, |
||||
} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
} |
@ -0,0 +1,21 @@
|
||||
import M from '../mutations.js' |
||||
|
||||
const state = [] // aka members (in this file):
|
||||
|
||||
const mutations = { |
||||
setCurrent(members, current) { |
||||
members.length = 0 |
||||
current.members.forEach(member => { |
||||
members.push(member) |
||||
}) |
||||
}, |
||||
applyEvent: M.membersMuts, |
||||
} |
||||
|
||||
const actions = {} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
} |
@ -0,0 +1,24 @@
|
||||
import M from '../mutations.js' |
||||
|
||||
const state = [] // aka files (in this file):
|
||||
|
||||
const mutations = { |
||||
setCurrent(memes, current) { |
||||
memes.length = 0 |
||||
if (!current.memes) { |
||||
current.memes = [] |
||||
} |
||||
current.memes.forEach(meme => { |
||||
memes.push(meme) |
||||
}) |
||||
}, |
||||
applyEvent: M.memesMuts, |
||||
} |
||||
|
||||
const actions = {} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
} |
@ -0,0 +1,21 @@
|
||||
import M from '../mutations.js' |
||||
|
||||
const state = [] // aka resources (in this file):
|
||||
|
||||
const mutations = { |
||||
setCurrent(resources, current) { |
||||
resources.length = 0 |
||||
current.resources.forEach(resource => { |
||||
resources.push(resource) |
||||
}) |
||||
}, |
||||
applyEvent: M.resourcesMuts, |
||||
} |
||||
|
||||
const actions = {} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
} |
@ -0,0 +1,21 @@
|
||||
import M from '../mutations.js' |
||||
|
||||
const state = [] |
||||
|
||||
const mutations = { |
||||
setCurrent(sessions, current) { |
||||
sessions.length = 0 |
||||
current.sessions.forEach(session => { |
||||
sessions.push(session) |
||||
}) |
||||
}, |
||||
applyEvent: M.sessionsMuts, |
||||
} |
||||
|
||||
const actions = {} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
} |
@ -0,0 +1,24 @@
|
||||
import M from '../mutations.js' |
||||
|
||||
const state = [] |
||||
|
||||
const mutations = { |
||||
setCurrent(tasks, current) { |
||||
tasks.forEach((task, i) => { |
||||
delete tasks[i] |
||||
}) |
||||
tasks.length = 0 |
||||
current.tasks.forEach((task, index) => { |
||||
tasks.push(task) |
||||
}) |
||||
}, |
||||
applyEvent: M.tasksMuts, |
||||
} |
||||
|
||||
const actions = {} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
} |
@ -0,0 +1,172 @@
|
||||
const modes = ['doge', 'boat', 'badge', 'chest', 'timecube'] |
||||
const payments = ['bitcoin', 'lightning'] |
||||
const dimensions = ['unicorn', 'sun', 'bull'] |
||||
|
||||
const state = { |
||||
mode: modes[0], |
||||
payment: false, |
||||
dimension: dimensions[0], |
||||
bird: false, |
||||
stacks: 1, |
||||
barking: false, |
||||
pinging: false, |
||||
zoom: false, |
||||
search: false, |
||||
searchResult: false, |
||||
paintbrushColor: false, |
||||
grid: { |
||||
selX: false, |
||||
selY: false, |
||||
}, |
||||
flashClasses: { |
||||
flash: false, |
||||
half: false, |
||||
twice: false, |
||||
five: false, |
||||
}, |
||||
} |
||||
|
||||
const mutations = { |
||||
flash(state) { |
||||
state.flashClasses.flash = true |
||||
}, |
||||
flashHalf(state) { |
||||
state.flashClasses.half = true |
||||
}, |
||||
flashTwice(state) { |
||||
state.flashClasses.twice = true |
||||
}, |
||||
flashFive(state) { |
||||
state.flashClasses.five = true |
||||
}, |
||||
flashOff(state) { |
||||
state.flashClasses.flash = false |
||||
state.flashClasses.half = false |
||||
state.flashClasses.twice = false |
||||
state.flashClasses.five = false |
||||
}, |
||||
toggleBird(state) { |
||||
state.bird = !state.bird |
||||
}, |
||||
toggleStacks(state) { |
||||
if (state.stacks === 5) { |
||||
state.stacks = 1 |
||||
} else { |
||||
state.stacks = 5 |
||||
} |
||||
}, |
||||
nextMode(state) { |
||||
let currentIndex = modes.indexOf(state.mode) |
||||
let nextIndex = (currentIndex + 1) % modes.length |
||||
state.mode = modes[nextIndex] |
||||
}, |
||||
previousMode(state) { |
||||
let currentIndex = modes.indexOf(state.mode) |
||||
let prevIndex = currentIndex <= 0 ? modes.length - 1 : currentIndex - 1 |
||||
state.mode = modes[prevIndex] |
||||
}, |
||||
setMode(state, index) { |
||||
state.mode = modes[index] |
||||
}, |
||||
closeUpgrades(state) { |
||||
state.mode = modes[0] |
||||
}, |
||||
setPayMode(state, index) { |
||||
state.payment = payments[index] |
||||
}, |
||||
closePayMode(state) { |
||||
state.payment = false |
||||
}, |
||||
setDimension(state, index) { |
||||
state.dimension = dimensions[index] |
||||
}, |
||||
closeDimension(state) { |
||||
state.dimension = false |
||||
}, |
||||
bark(state) { |
||||
state.barking = true |
||||
state.pinging = true |
||||
// XXX - should be sync? Works!?
|
||||
setTimeout(() => { |
||||
state.barking = false |
||||
}, 1000) |
||||
setTimeout(() => { |
||||
state.pinging = false |
||||
}, 2000) |
||||
//let flip = new Audio(import('../sounds/ping.wav'))
|
||||
//flip.volume = flip.volume * 0.33
|
||||
//flip.play()
|
||||
}, |
||||
zoom(state) { |
||||
state.zoom = !state.zoom |
||||
}, |
||||
search(state, query) { |
||||
state.search = query |
||||
}, |
||||
selectSearchResult(state, taskId) { |
||||
state.searchResult = taskId |
||||
console.log('search result loaded ', taskId) |
||||
}, |
||||
searchSelectionReceived(state, taskId) { |
||||
state.searchResult = false |
||||
}, |
||||
startPainting(state, color) { |
||||
console.log('starting painting') |
||||
state.paintbrushColor = color |
||||
}, |
||||
stopPainting(state) { |
||||
state.paintbrushColor = false |
||||
}, |
||||
} |
||||
|
||||
const actions = { |
||||
nextUpgradeMode({ commit, state }, router) { |
||||
commit('nextMode') |
||||
commit('startLoading', state.mode) |
||||
|
||||
if (state.dimension === 'sun') { |
||||
return router.push('/front/' + state.mode) |
||||
} |
||||
if (state.dimension === 'bull') { |
||||
return router.push('/dash/' + state.mode) |
||||
} |
||||
router.push('/' + state.mode) |
||||
}, |
||||
previousUpgradeMode({ commit, state }, router) { |
||||
commit('previousMode') |
||||
commit('startLoading', state.mode) |
||||
|
||||
if (state.dimension === 'sun') { |
||||
return router.push('/front/' + state.mode) |
||||
} |
||||
if (state.dimension === 'bull') { |
||||
return router.push('/dash/' + state.mode) |
||||
} |
||||
router.push('/' + state.mode) |
||||
}, |
||||
flashHelm({ commit, state }, flashes) { |
||||
commit('flash') |
||||
let ms = 350 |
||||
if (flashes < 1) { |
||||
commit('flashHalf') |
||||
ms *= 0.7 |
||||
} else if (flashes === 2) { |
||||
commit('flashTwice') |
||||
ms *= flashes |
||||
} else if (flashes === 5) { |
||||
commit('flashFive') |
||||
ms *= flashes |
||||
} |
||||
setTimeout(() => { |
||||
commit('flashOff') |
||||
}, ms) |
||||
}, |
||||
} |
||||
const getters = {} |
||||
|
||||
export default { |
||||
state, |
||||
mutations, |
||||
actions, |
||||
getters, |
||||
} |
@ -0,0 +1,132 @@
|
||||
const defaultSemantics = { |
||||
glossary: { |
||||
card: 'card', |
||||
user: 'member', |
||||
username: 'hackername', |
||||
proposal: 'proposition', |
||||
avatar: 'avatar', |
||||
}, |
||||
levels: { |
||||
0: 'guest', |
||||
1: 'member', |
||||
2: 'elite member', |
||||
}, |
||||
} |
||||
|
||||
let loadedGlossary = {} |
||||
/* Too complex for .env, need a new solution, maybe use fs here to import custom JSON or YAML file |
||||
if (config.semantics && config.semantics.glossary) { |
||||
loadedGlossary = config.semantics.glossary |
||||
}*/ |
||||
const serverGlossary = { ...defaultSemantics.glossary, ...loadedGlossary } |
||||
|
||||
function pluralize(word) { |
||||
let plural = word |
||||
if (Array.isArray(plural)) { |
||||
plural = plural[1] |
||||
} else { |
||||
if (plural[plural.length - 1] === 's') { |
||||
plural = plural + 'es' |
||||
} else { |
||||
plural = plural + 's' |
||||
} |
||||
} |
||||
return plural |
||||
} |
||||
|
||||
export function capitalize(word) { |
||||
if (word.length < 1) { |
||||
return '' |
||||
} |
||||
return word[0].toUpperCase() + word.substring(1) |
||||
} |
||||
|
||||
// Returns the given word or string, with all instances of words in the glossary in configuration.js replaced with their gloss.
|
||||
// In a multi-word string, it will correctly distinguish between all-lowercase keywords and those
|
||||
// with their first letter capitalized, and replace them correctly. Original hardcoded keywords must be typed (in this codebase)
|
||||
// in either all lowercase or with the first letter capitalized to be caught be the word replacement.
|
||||
export function gloss(wordOrSentence, plural = false) { |
||||
let result |
||||
if (wordOrSentence.indexOf(' ') < 0) { |
||||
const word = wordOrSentence |
||||
|
||||
result = word |
||||
const lowercase = word.toLowerCase() |
||||
const pluralEntry = Object.entries(serverGlossary).find( |
||||
([keyword, synonym]) => { |
||||
return ( |
||||
(Array.isArray(keyword) && keyword[1] === lowercase) || |
||||
pluralize(keyword) === lowercase |
||||
) |
||||
} |
||||
) |
||||
const singularEntry = Object.entries(serverGlossary).find( |
||||
([keyword, synonym]) => |
||||
(Array.isArray(keyword) && keyword[0] === lowercase) || |
||||
keyword === lowercase |
||||
) |
||||
if (pluralEntry || singularEntry) { |
||||
result = pluralEntry ? pluralize(pluralEntry[1]) : singularEntry[1] |
||||
if (Array.isArray(result)) { |
||||
result = result[0] |
||||
} |
||||
if (word[0].toLowerCase() !== word[0]) { |
||||
result = result[0].toUpperCase() + result.substring(1) |
||||
} |
||||
} |
||||
} else { |
||||
result = wordOrSentence |
||||
|
||||
Object.entries(serverGlossary).forEach(([keyword, synonym]) => { |
||||
// replace lowercase plural version of the keyword
|
||||
const pluralKeyword = pluralize(keyword) |
||||
const pluralSynonym = pluralize(synonym) |
||||
let regexp = new RegExp('\\b' + pluralKeyword + '\\b', 'g') |
||||
result = result.replace(regexp, pluralSynonym) |
||||
|
||||
// replace capitalized plural version of the keyword
|
||||
const pluralKeywordUppercase = capitalize(pluralKeyword) |
||||
const pluralSynonymUppercase = capitalize(pluralSynonym) |
||||
regexp = new RegExp('\\b' + pluralKeywordUppercase + '\\b', 'g') |
||||
result = result.replace(regexp, pluralSynonymUppercase) |
||||
|
||||
// replace lowercase singular version of the keyword
|
||||
regexp = new RegExp('\\b' + keyword + '\\b', 'g') |
||||
const singularSynonym = Array.isArray(synonym) ? synonym[0] : synonym |
||||
result = result.replace(regexp, singularSynonym) |
||||
|
||||
// replace capitalized singular version of the keyword
|
||||
const singularKeywordUppercase = capitalize(keyword) |
||||
const singularSynonymUppercase = capitalize(singularSynonym) |
||||
regexp = new RegExp('\\b' + singularKeywordUppercase + '\\b', 'g') |
||||
result = result.replace(regexp, singularSynonymUppercase) |
||||
}) |
||||
} |
||||
return result |
||||
} |
||||
|
||||
let loadedLevels = {} |
||||
/*if (config.semantics && config.semantics.levels) { |
||||
loadedLevels = config.semantics.levels |
||||
}*/ |
||||
const serverLevels = { ...defaultSemantics.levels, ...loadedLevels } |
||||
|
||||
export function glossLevel(level) { |
||||
if (level < 0) { |
||||
return null |
||||
} |
||||
let highestMatchingWord |
||||
Object.entries(serverLevels).some(([index, word]) => { |
||||
if (index <= level) { |
||||
highestMatchingWord = word |
||||
} |
||||
if (index >= level) { |
||||
return true |
||||
} |
||||
}) |
||||
return highestMatchingWord || 'member' |
||||
} |
||||
|
||||
export function getSemantics() { |
||||
return { glossary: serverGlossary, levels: serverLevels } |
||||
} |
@ -0,0 +1,172 @@
|
||||
// entry point for server
|
||||
|
||||
//import 'vite/modulepreload-polyfill'
|
||||
|
||||
let PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 8003 |
||||
console.log('AO listening on PORT: ', PORT) |
||||
|
||||
import dotenv from 'dotenv' |
||||
try { |
||||
const result = dotenv.config({ path: './.env', override: true }) |
||||
if (result.error) { |
||||
console.log('Error parsing server config file .env.server') |
||||
throw result.error |
||||
} |
||||
console.log( |
||||
'Loaded server environment variables from .env.server:', |
||||
result.parsed |
||||
) |
||||
} catch { |
||||
console.log('No .env file found. Variables put here will be only accessible on the server.') |
||||
} |
||||
|
||||
import express from 'express' |
||||
import { merge } from 'kefir' |
||||
import { Server } from 'socket.io' |
||||
import socketProtector from 'socketio-auth' |
||||
import { startDb, changeFeed, shadowFeed, insertEvent } from './database.js' |
||||
import state from './state.js' |
||||
import reactions from './reactions.js' |
||||
import applyRouter from './router.js' |
||||
import { socketAuth } from './auth.js' |
||||
import rent from './rent.js' |
||||
import link from './link.js' |
||||
import cleanup from './cleanup.js' |
||||
import todo from './todo.js' |
||||
import { scanMemes } from './files.js' |
||||
import { recordEveryInvoice, watchOnChain } from './lightning.js' |
||||
|
||||
export const app = express() |
||||
applyRouter(app) |
||||
|
||||
const AO_LIGHTNING_ENABLED = process.env.AO_LIGHTNING_ENABLED && (process.env.AO_LIGHTNING_ENABLED === '1' || process.env.AO_LIGHTNING_ENABLED.toLowerCase() ==='true') |
||||
export default async function startAoServer() { |
||||
console.log('starting AO database...') |
||||
let dbPath = process.env.AO_DB_FILE |
||||
if (PORT !== 8003) { |
||||
console.log( |
||||
'Port is not set to the default of 8003, using alternate database.' |
||||
) |
||||
dbPath = dbPath.replace('database', PORT.toString()) |
||||
} |
||||
return startDb(dbPath, (err, conn) => { |
||||
let start = Date.now() |
||||
state.initialize(err => { |
||||
if (err) return console.log('state initialize failed:', err) |
||||
rent() |
||||
link() |
||||
scanMemes() |
||||
todo() |
||||
cleanup() |
||||
if ( |
||||
AO_LIGHTNING_ENABLED |
||||
) { |
||||
recordEveryInvoice(state.serverState.cash.pay_index) |
||||
watchOnChain() |
||||
} |
||||
const serverReactions = changeFeed |
||||
.onValue(ev => { |
||||
state.applyEvent(state.serverState, ev) |
||||
}) |
||||
.onValue(reactions) |
||||
|
||||
const server = app.listen(PORT, err => { |
||||
console.log('Listening on port', PORT) |
||||
|
||||
// TODO continue merge argument about this functionality
|
||||
// likely best option is to use an ENV variable to determine
|
||||
// if we are behind a proxy or not. This is likely useful / necessary
|
||||
// also for express configuration
|
||||
// const ioServer = new Server(server, {
|
||||
// cors: {
|
||||
// origin: ['http://127.0.0.1:3000', 'http://localhost:3000'],
|
||||
// methods: ['GET', 'POST'],
|
||||
// },
|
||||
// })
|
||||
|
||||
// ioServer.listen(PORT)
|
||||
const ioServer = new Server(server, { |
||||
cors: { |
||||
origin: [ |
||||
'http://localhost:3000/main.js', |
||||
'http://127.0.0.1:3000' + |
||||
(process.env.mode === 'production' ? ':' + PORT : ''), |
||||
'http://localhost:3000' + |
||||
(process.env.mode === 'production' ? ':' + PORT : ''), |
||||
'http://127.0.0.1:3000', |
||||
'http://localhost:3000', |
||||
], |
||||
methods: ['GET', 'POST'], |
||||
}, |
||||
}) |
||||
|
||||
socketProtector(ioServer, { |
||||
authenticate: socketAuth, |
||||
timeout: 2000, |
||||
}) |
||||
|
||||
const filteredStream = changeFeed.map(state.removeSensitive) |
||||
|
||||
const fullEvStream = merge([filteredStream, shadowFeed]) |
||||
|
||||
fullEvStream.onValue(ev => { |
||||
state.applyEvent(state.pubState, ev) |
||||
ioServer.emit('eventstream', ev) |
||||
console.log('emitting:', ev.type) |
||||
}) |
||||
|
||||
// ensure there is a community hub card in the state
|
||||
// console.log("AO: server/state.js: initialize: checking for community hub card", { "tasks": state.pubState.tasks } );
|
||||
// let communityHubCardFound = false;
|
||||
// state.pubState.tasks.forEach
|
||||
// ( (taskItem, index) =>
|
||||
// {
|
||||
// if (taskItem.name.toLowerCase() === "community hub")
|
||||
// {
|
||||
// communityHubCardFound = true;
|
||||
// console.log("AO: server/state.js: initialize: community hub card found");
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// if (communityHubCardFound === false)
|
||||
// {
|
||||
// let newCommunityHubCardEvent =
|
||||
// {
|
||||
// type : "task-create",
|
||||
// name : "community hub",
|
||||
// color : "blue",
|
||||
// deck : [],
|
||||
// inId : null,
|
||||
// prioritized : false,
|
||||
// }
|
||||
// // setImmediate
|
||||
// // (
|
||||
// // () =>
|
||||
// // {
|
||||
// insertEvent
|
||||
// ( newCommunityHubCardEvent,
|
||||
// (error, {event, result}) =>
|
||||
// {
|
||||
|
||||
// if (error)
|
||||
// {
|
||||
// // this should never happen... umm... not sure what to do here
|
||||
// console.log("AO: server/state.js: initialize: error running insertEvent for communityHubCard", {error, event, result});
|
||||
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // we should be good to go to send data to any clients.
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// // }
|
||||
// // );
|
||||
// }
|
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
startAoServer() |
@ -0,0 +1,99 @@
|
||||
import { buildResCallback } from './utils.js' |
||||
import events from './events.js' |
||||
import { createHash, hmacHex } from '../crypto.js' |
||||
import state from './state.js' |
||||
|
||||
const getIdSecret = function (identifier) { |
||||
var ownerId, secret |
||||
|
||||
try { |
||||
identifier = identifier.toLowerCase() |
||||
} catch (err) {} |
||||
|
||||
state.serverState.members.forEach(member => { |
||||
let name |
||||
try { |
||||
name = member.name.toLowerCase() |
||||
} catch (err) {} |
||||
if (name === identifier || member.memberId === identifier) { |
||||
ownerId = member.memberId |
||||
secret = member.secret |
||||
} |
||||
}) |
||||
|
||||
state.serverState.resources.forEach(resource => { |
||||
if (resource.name === identifier || resource.resourceId === identifier) { |
||||
ownerId = resource.resourceId |
||||
secret = resource.secret |
||||
} |
||||
}) |
||||
|
||||
return { ownerId, secret } |
||||
} |
||||
// Used in socketio-auth creation, checks token (https://www.npmjs.com/package/socketio-auth)
|
||||
export function socketAuth(socket, data, callback) { |
||||
let authorized |
||||
state.serverState.sessions.forEach(session => { |
||||
if (session.token === data.token) { |
||||
authorized = true |
||||
} |
||||
}) |
||||
console.log('socket auth triggered:', authorized) |
||||
callback(null, authorized) |
||||
} |
||||
|
||||
// This function receives an authentication request and processes it
|
||||
export function serverAuth(req, res, next) { |
||||
// Firstly, it looks through the server's list of members to see if the username
|
||||
// matches an active profile, and returns their secret if it does
|
||||
const { ownerId, secret } = getIdSecret(req.headers.name) |
||||
|
||||
// If the user is actively providing a password, use that for authorization.
|
||||
// Otherwise, attempt to use an available token in cookies
|
||||
const authorization = req.headers.authorization || req.cookies.token |
||||
|
||||
if (secret && req.headers.session) { |
||||
// If the user is in the list of members (secret)
|
||||
// and has initiated a session (doesn't happen on auto-login), then proceed
|
||||
// with the creation of a new session.
|
||||
console.log('Auth hashes') |
||||
let sessionKey = createHash(req.headers.session + secret) |
||||
let token = hmacHex(req.headers.session, sessionKey) |
||||
|
||||
// Go through the same process to create the token and see if it matches the
|
||||
// authorization provided
|
||||
if (token === authorization) { |
||||
// client able to create the token, must have secret
|
||||
res.cookie('token', token, { |
||||
httpOnly: true, |
||||
expires: new Date(253402300000000), |
||||
}) |
||||
events.trigger( |
||||
'session-created', |
||||
{ |
||||
session: req.headers.session, |
||||
token: token, |
||||
ownerId: ownerId, |
||||
}, |
||||
buildResCallback(res, { memberId: ownerId }) |
||||
) |
||||
} else { |
||||
res.status(401).end('unauthorized') |
||||
} |
||||
} else { |
||||
// If they are not passing a session ID or a username, then we check their
|
||||
// authorization against the list of active sessions and look for a match
|
||||
let authorized = false |
||||
state.serverState.sessions.forEach(session => { |
||||
if (session.token === authorization) { |
||||
authorized = true |
||||
req.reqOwner = session.ownerId |
||||
} |
||||
}) |
||||
if (authorized) { |
||||
next() |
||||
} else { |
||||
res.status(401).end('unauthorized') |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,48 @@
|
||||
import { loadMeme } from './files.js' |
||||
import YoutubeDlWrap from 'youtube-dl-wrap' |
||||
|
||||
const youtubeDlWrap = new YoutubeDlWrap(process.env.AO_MEMES_VIDEO_CACHER_PATH) |
||||
|
||||
export async function cache(url, taskId) { |
||||
if ( |
||||
!process.env.AO_MEMES_DIR || |
||||
process.env.AO_MEMES_VIDEO_CACHER_ENABLED !== '1' |
||||
) { |
||||
console.log( |
||||
'No memes directory or videoCacher specified in .env.server. See readme.' |
||||
) |
||||
} |
||||
let stdout = await youtubeDlWrap |
||||
.execPromise([ |
||||
url, |
||||
'-f', |
||||
'best', |
||||
'-o', |
||||
process.env.AO_MEMES_DIR + '/%(title)s-%(uploader)s-%(id)s.%(ext)s', |
||||
]) |
||||
.catch(err => { |
||||
console.log('error caching:', err) |
||||
}) |
||||
|
||||
console.log('cached:' + url) |
||||
// Extract filename from returned console output (event listener method is more elegant)
|
||||
let newFilename |
||||
let newPath |
||||
try { |
||||
const query = 'Destination: (.+)' |
||||
const regex = new RegExp(query, 'i') |
||||
const matched = regex.exec(stdout) |
||||
console.log('matched path is ', matched) |
||||
if (matched.length >= 2) { |
||||
newPath = matched[1] |
||||
const parts = newPath.split('/') |
||||
newFilename = parts[parts.length - 1] |
||||
} |
||||
} catch (err) {} |
||||
|
||||
if (newPath && newFilename) { |
||||
loadMeme(newFilename, newPath, taskId) |
||||
} else { |
||||
console.log('Meme was uploaded, but failed to add it to a card') |
||||
} |
||||
} |
@ -0,0 +1,51 @@
|
||||
import cron from 'cron' |
||||
import events from './events.js' |
||||
import state from './state.js' |
||||
const serverState = state.serverState |
||||
|
||||
// Six minutes - one minute grace period, deleted every five minutes
|
||||
const deleteAfterMs = 6 * 60 * 1000 |
||||
|
||||
// Make sure this makes sense with deleteAfterMs
|
||||
const cleanupJob = new cron.CronJob({ |
||||
cronTime: '5 * * * * *', |
||||
onTick: cleanup, |
||||
start: true, |
||||
timeZone: 'America/Los_Angeles', |
||||
}) |
||||
|
||||
function cleanup() { |
||||
const beforeCount = serverState.tasks.length |
||||
const oldUnheldCards = serverState.tasks |
||||
.filter(t => { |
||||
const isUnheld = t.deck.length <= 0 |
||||
const isOld = Date.now() - t.created > deleteAfterMs |
||||
const isMemberCard = |
||||
t.taskId === t.name || |
||||
serverState.members.some(member => member.memberId === t.taskId) |
||||
const isReservedCard = ['community hub'].includes(t.name) |
||||
return isUnheld && isOld && !isMemberCard && !isReservedCard |
||||
}) |
||||
.map(t => t.taskId) |
||||
|
||||
if (oldUnheldCards.length <= 0) { |
||||
console.log('No shitposts to clean up') |
||||
return |
||||
} |
||||
events.trigger( |
||||
'tasks-removed', |
||||
{ taskIds: oldUnheldCards, blame: 'cleanup' }, |
||||
null |
||||
) |
||||
|
||||
const removedCount = beforeCount - serverState.tasks.length |
||||
console.log( |
||||
'Cleaned up', |
||||
removedCount, |
||||
'shitpost' + (removedCount >= 2 ? 's' : '') |
||||
) |
||||
} |
||||
|
||||
export default function () { |
||||
cleanupJob.start() |
||||
} |
@ -0,0 +1,59 @@
|
||||
import express from 'express' |
||||
const router = express.Router() |
||||
import tr from 'tor-request' |
||||
|
||||
export function postEvent(address, secret, body, callback, logErrors = true) { |
||||
tr.request( |
||||
{ |
||||
url: 'http://' + address + '/events', |
||||
headers: { Authorization: secret }, |
||||
method: 'post', |
||||
body, |
||||
json: true, |
||||
}, |
||||
function (err, res, resBody) { |
||||
if (err) { |
||||
if (logErrors) console.log('error res', err) |
||||
return callback(err) |
||||
} |
||||
callback(resBody) |
||||
} |
||||
) |
||||
} |
||||
|
||||
export function checkHash(address, secret, taskId, callback, logErrors = true) { |
||||
tr.request( |
||||
{ |
||||
url: 'http://' + address + '/taskhash/' + taskId, |
||||
headers: { Authorization: secret }, |
||||
method: 'post', |
||||
json: true, |
||||
}, |
||||
function (err, res, resBody) { |
||||
if (err) { |
||||
if (logErrors) console.log('error res', err) |
||||
return callback(err) |
||||
} |
||||
callback(resBody) |
||||
} |
||||
) |
||||
} |
||||
|
||||
export function getState(address, secret, callback) { |
||||
tr.request( |
||||
{ |
||||
url: 'http://' + address + '/state', |
||||
headers: { Authorization: secret }, |
||||
method: 'post', |
||||
body: { x: true }, |
||||
json: true, |
||||
}, |
||||
function (err, res, resBody) { |
||||
if (err) { |
||||
console.log('error res', err) |
||||
return callback(err) |
||||
} |
||||
callback(null, resBody) |
||||
} |
||||
) |
||||
} |
@ -0,0 +1,235 @@
|
||||
import Kefir from 'kefir' |
||||
import { v1 } from 'uuid' |
||||
import dbengine from 'better-sqlite3' |
||||
import { createHash } from '../crypto.js' |
||||
import { blankCard } from '../cards.js' |
||||
import { Task } from '../types.js' |
||||
|
||||
interface TaskCreatedEvent extends Task { |
||||
type?: string |
||||
} |
||||
|
||||
let preparedStmts = {} |
||||
|
||||
export var conn |
||||
|
||||
var eventEmitter, shadowEmitter |
||||
|
||||
export const changeFeed = Kefir.stream(e => { |
||||
eventEmitter = e |
||||
}) |
||||
|
||||
export const shadowFeed = Kefir.stream(e => { |
||||
shadowEmitter = e |
||||
}) |
||||
|
||||
export function triggerShadow(x) { |
||||
shadowEmitter.emit(x) |
||||
} |
||||
|
||||
function initializeSqlite(cb) { |
||||
console.log('initializing new sqlite3') |
||||
var err = null |
||||
try { |
||||
var initDb = conn.prepare( |
||||
'CREATE TABLE `events` ( `document` BLOB NOT NULL, `timestamp` INTEGER UNIQUE, PRIMARY KEY(`timestamp`) )' |
||||
) |
||||
var initBackups = conn.prepare( |
||||
'CREATE TABLE `backups` ( `document` BLOB NOT NULL, `timestamp` INTEGER UNIQUE, PRIMARY KEY(`timestamp`) )' |
||||
) |
||||
initDb.run() |
||||
initBackups.run() |
||||
createStatements() |
||||
} catch (actualErr) { |
||||
console.log(actualErr) |
||||
err = actualErr |
||||
} |
||||
if (err) { |
||||
cb(err, conn) |
||||
} else { |
||||
const firstMemberId = v1() |
||||
const now = Date.now() |
||||
insertEvent({ |
||||
type: 'member-created', |
||||
name: 'ao', |
||||
fob: '0000000000', |
||||
secret: createHash('ao'), // initial user-password is ao
|
||||
memberId: firstMemberId, |
||||
address: '2Mz6BQSTkmK4WHCntwNfvdSfWHddTqQX4vu', |
||||
active: 1, |
||||
balance: 0, |
||||
badges: [], |
||||
info: {}, |
||||
}) |
||||
const bookmarksCardTaskId = v1() |
||||
let bookmarksCardEvent: TaskCreatedEvent = blankCard( |
||||
bookmarksCardTaskId, |
||||
firstMemberId + '-bookmarks', |
||||
'blue', |
||||
now, |
||||
[firstMemberId] |
||||
) |
||||
bookmarksCardEvent.type = 'task-created' |
||||
insertEvent(bookmarksCardEvent) |
||||
insertEvent({ |
||||
type: 'grid-added', |
||||
taskId: bookmarksCardTaskId, |
||||
height: 1, |
||||
width: 1, |
||||
spread: 'grid', |
||||
}) |
||||
|
||||
let blankCardEvent: TaskCreatedEvent = blankCard( |
||||
v1(), |
||||
'community hub', |
||||
'yellow', |
||||
now, |
||||
[firstMemberId] |
||||
) |
||||
blankCardEvent.type = 'task-created' |
||||
blankCardEvent.guild = 'Community Hub' |
||||
insertEvent(blankCardEvent) |
||||
startFeed() |
||||
cb(null, conn) |
||||
} |
||||
} |
||||
|
||||
function createStatements() { |
||||
// console.log("AO: server/dctrlDb.js: createStatements");
|
||||
conn.function('eventFeed', doc => { |
||||
eventEmitter.emit(JSON.parse(doc)) |
||||
}) |
||||
preparedStmts['getAll'] = conn.prepare( |
||||
'SELECT document FROM events WHERE (timestamp > ?) ORDER BY timestamp' |
||||
) // WHERE (timestamp > ?)
|
||||
preparedStmts['insertEvent'] = conn.prepare( |
||||
'INSERT INTO events VALUES (?, ?)' |
||||
) |
||||
preparedStmts['insertBackup'] = conn.prepare( |
||||
'INSERT INTO backups VALUES (?, ?)' |
||||
) |
||||
preparedStmts['recover'] = conn.prepare( |
||||
'SELECT document from backups ORDER BY timestamp DESC LIMIT 1' |
||||
) |
||||
} |
||||
|
||||
export function recover(callback) { |
||||
let all = [] |
||||
try { |
||||
for (const ev of preparedStmts['recover'].iterate()) { |
||||
console.log |
||||
all.push(JSON.parse(ev.document)) |
||||
} |
||||
callback(null, all) |
||||
} catch (err) { |
||||
console.log( |
||||
'database recover error: ', |
||||
err, |
||||
'for event', |
||||
all[all.length - 1] |
||||
) |
||||
} |
||||
} |
||||
|
||||
export function getAll(timestamp, callback) { |
||||
let all = [] |
||||
try { |
||||
for (const ev of preparedStmts['getAll'].iterate(timestamp)) { |
||||
all.push(JSON.parse(ev.document)) |
||||
} |
||||
callback(null, all) |
||||
} catch (err) { |
||||
console.log( |
||||
'database getAll error: ', |
||||
err, |
||||
'for event', |
||||
all[all.length - 1] |
||||
) |
||||
} |
||||
} |
||||
|
||||
function startFeed() { |
||||
conn.function('eventFeed', doc => { |
||||
eventEmitter.emit(JSON.parse(doc)) |
||||
}) |
||||
try { |
||||
conn |
||||
.prepare( |
||||
'CREATE TRIGGER updateHook AFTER INSERT ON events BEGIN SELECT eventFeed(NEW.document); END' |
||||
) |
||||
.run() |
||||
} catch (error) { |
||||
console.log('AO: server/database.ts: startFeed: error running conn prepare') |
||||
} |
||||
} |
||||
|
||||
export function insertEvent(ev, callback?) { |
||||
if (ev.type === 'tasks-received') { |
||||
console.log(`received ${ev?.tasks?.length} tasks`) |
||||
ev?.tasks?.forEach(task => console.log('p2p:', task.name)) |
||||
console.log('p2p: end of receive') |
||||
} else { |
||||
console.log('insertEvent ev is ', ev) |
||||
} |
||||
if (!conn) return callback('No db connection') |
||||
if (!ev.timestamp) { |
||||
ev.timestamp = Date.now() |
||||
} |
||||
var err = null |
||||
var result = null |
||||
try { |
||||
result = preparedStmts['insertEvent'].run(JSON.stringify(ev), ev.timestamp) |
||||
} catch (actualErr) { |
||||
err = actualErr |
||||
} |
||||
if (callback) { |
||||
callback(err, result) |
||||
} |
||||
} |
||||
|
||||
export function insertBackup(state, callback?) { |
||||
if (!conn) return callback('No db connection') |
||||
|
||||
state.timestamp = Date.now() |
||||
|
||||
var err = null |
||||
var result = null |
||||
try { |
||||
result = preparedStmts['insertBackup'].run( |
||||
JSON.stringify(state), |
||||
state.timestamp |
||||
) |
||||
} catch (actualErr) { |
||||
err = actualErr |
||||
} |
||||
if (callback) return callback(err, result) |
||||
} |
||||
|
||||
export function startDb(path, callback) { |
||||
conn = dbengine(path, {}) |
||||
var checkTable = conn.prepare( |
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='events'" |
||||
) |
||||
|
||||
if (checkTable.all().length == 0) { |
||||
initializeSqlite(callback) |
||||
} else { |
||||
createStatements() |
||||
// startFeed()
|
||||
callback(null, conn) |
||||
} |
||||
} |
||||
|
||||
export function verifyAndLoadDb(path) { |
||||
conn = dbengine(path, {}) |
||||
var checkTable = conn.prepare( |
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='events'" |
||||
) |
||||
|
||||
if (checkTable.all().length == 0) { |
||||
return 'Database does not exist or does not have AO tables in it.' |
||||
} else { |
||||
createStatements() |
||||
} |
||||
return true |
||||
} |
@ -0,0 +1,27 @@
|
||||
import { insertEvent, triggerShadow } from './database.js' |
||||
|
||||
function trigger(type, eventData, callback?) { |
||||
let newEvent = { type, ...eventData } |
||||
insertEvent(newEvent, callback) |
||||
} |
||||
|
||||
function triggerShadowPlease(type, eventData, callback) { |
||||
let newEvent = { type, ...eventData } |
||||
triggerShadow(newEvent) |
||||
callback() // db call is synchronous (?) so just do it here I guess
|
||||
} |
||||
|
||||
function getNodeInfo(info, callback?) { |
||||
let newEvent = { |
||||
type: 'get-node-info', |
||||
info, |
||||
} |
||||
triggerShadow(newEvent) |
||||
callback() |
||||
} |
||||
|
||||
export default { |
||||
triggerShadowPlease, |
||||
trigger, |
||||
getNodeInfo, |
||||
} |
@ -0,0 +1,92 @@
|
||||
import path from 'path' |
||||
import fs from 'fs' |
||||
import events from './events.js' |
||||
import { createHash } from '../crypto.js' |
||||
import state from './state.js' |
||||
import { v1 } from 'uuid' |
||||
const serverState = state.serverState |
||||
|
||||
export function scanMemes() { |
||||
const homeDir = |
||||
process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE |
||||
const memeFolder = process.env.AO_MEMES_DIR |
||||
console.log('\nmemeFolder is ', memeFolder) |
||||
|
||||
fs.readdir(memeFolder, function (err, files) { |
||||
if (err) { |
||||
return console.log('Failed to scan memes: ' + err) |
||||
} |
||||
console.log('\nfolder contains ', files.length, ' memes') |
||||
files.forEach(filename => { |
||||
// console.log(filename)
|
||||
const filepath = path.join(memeFolder, filename) |
||||
loadMeme(filename, filepath) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
export function loadMeme(name, path, taskId = null) { |
||||
fs.readFile(path, (err, data) => { |
||||
if (err) { |
||||
console.log('Directory or other error-causing file found, ignoring') |
||||
return |
||||
} |
||||
addMeme(name, path, data, taskId) |
||||
}) |
||||
} |
||||
|
||||
export async function addMeme(name, path, data = null, taskId = null) { |
||||
// console.log('addMeme function')
|
||||
if (!data) { |
||||
return new Promise((resolve, reject) => { |
||||
fs.readFile(path, (err, data) => { |
||||
if (err) { |
||||
console.log('Directory or other error-causing file found, ignoring') |
||||
reject(err) |
||||
} else if (data) { |
||||
console.log('going deeper in addMeme') |
||||
resolve(addMeme(name, path, data, taskId)) |
||||
} else { |
||||
console.log('readFile failed') |
||||
reject(false) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
const hash = createHash(data) |
||||
const lastIndex = name.lastIndexOf('.') |
||||
const filetype = lastIndex < 0 ? '' : name.substr(lastIndex + 1) |
||||
|
||||
// console.log(`${hash} ${name}`)
|
||||
const foundMeme = serverState.memes.find(m => { |
||||
return m.hash === hash |
||||
}) |
||||
// console.log('foundMeme is', foundMeme)
|
||||
if (foundMeme) { |
||||
// console.log('returning existing meme: ', foundMeme.memeId)
|
||||
return Promise.resolve(foundMeme.memeId) |
||||
} |
||||
|
||||
const newTaskId = taskId || v1() |
||||
console.log('returning new promise') |
||||
return new Promise((resolve, reject) => { |
||||
events.trigger( |
||||
'meme-added', |
||||
{ taskId: newTaskId, filename: name, hash, filetype }, |
||||
(err, event) => { |
||||
console.log( |
||||
'\n\n\nmeme-added callback\n\nerr: ', |
||||
err, |
||||
'\n event: ', |
||||
event, |
||||
'\n\n', |
||||
'newTaskId: ', |
||||
newTaskId |
||||
) |
||||
console.log('newTaskId is about to resolve:', newTaskId) |
||||
resolve(newTaskId) |
||||
} |
||||
) |
||||
}) |
||||
} |
@ -0,0 +1,46 @@
|
||||
import express from 'express' |
||||
const router = express.Router() |
||||
|
||||
import state from './state.js' |
||||
import { memberFromFob, getResource, buildResCallback } from './utils.js' |
||||
import events from './events.js' |
||||
|
||||
function access(member, resource) { |
||||
if (member.active < 0) { |
||||
return false |
||||
} |
||||
if (resource.charged == 0) { |
||||
return true |
||||
} |
||||
let newBalance = member.balance - resource.charged |
||||
return newBalance >= 0 |
||||
} |
||||
|
||||
function resourceCheck(req, res, next) { |
||||
console.log('resourceCheck') |
||||
let member = memberFromFob(req.body.fob) |
||||
let resource = getResource(req.body.resourceId) |
||||
if (member && resource && access(member, resource)) { |
||||
events.trigger( |
||||
'resource-used', |
||||
{ |
||||
resourceId: req.body.resourceId, |
||||
memberId: member.memberId, |
||||
amount: req.body.amount || 1, |
||||
charged: resource.charged || 0, |
||||
notes: req.body.notes || 'D', |
||||
}, |
||||
buildResCallback(res) |
||||
) |
||||
} else { |
||||
next() |
||||
} |
||||
} |
||||
|
||||
router.use('/fobtap', resourceCheck) |
||||
|
||||
router.use('/fobtap', (req, res) => { |
||||
res.end('fobtap not handled') |
||||
}) |
||||
|
||||
export default router |
@ -0,0 +1,273 @@
|
||||
import path from 'path' |
||||
import net from 'net' |
||||
import debug from 'debug' |
||||
import { EventEmitter } from 'events' |
||||
import chalk from 'chalk' |
||||
|
||||
const debugLightning = debug('lightning-client') |
||||
|
||||
const methods = [ |
||||
'autocleaninvoice', |
||||
'check', |
||||
'checkmessage', |
||||
'close', |
||||
'connect', |
||||
'createonion', |
||||
'decodepay', |
||||
'delexpiredinvoice', |
||||
'delinvoice', |
||||
'delpay', |
||||
'dev-sendcustommsg', |
||||
'disconnect', |
||||
'feerates', |
||||
'fundchannel', |
||||
'fundchannel_cancel', |
||||
'fundchannel_complete', |
||||
'fundchannel_start', |
||||
'fundpsbt', |
||||
'getinfo', |
||||
'getlog', |
||||
'getroute', |
||||
'getsharedsecret', |
||||
'help', |
||||
'hsmtool', |
||||
'invoice', |
||||
'keysend', |
||||
'listchannels', |
||||
'listconfigs', |
||||
'listforwards', |
||||
'listfunds', |
||||
'listinvoices', |
||||
'listnodes', |
||||
'listpays', |
||||
'listpeers', |
||||
'listsendpays', |
||||
'listtransactions', |
||||
'multifundchannel', |
||||
'multiwithdraw', |
||||
'newaddr', |
||||
'notifications', |
||||
'openchannel_init', |
||||
'openchannel_signed', |
||||
'openchannel_update', |
||||
'pay', |
||||
'ping', |
||||
'plugin', |
||||
'reserveinputs', |
||||
'sendonion', |
||||
'sendpay', |
||||
'sendpsbt', |
||||
'setchannelfee', |
||||
'signmessage', |
||||
'signpsbt', |
||||
'stop', |
||||
'txdiscard', |
||||
'txprepare', |
||||
'txsend', |
||||
'unreserveinputs', |
||||
'utxopsbt', |
||||
'waitanyinvoice', |
||||
'waitblockheight', |
||||
'waitinvoice', |
||||
'waitsendpay', |
||||
'withdraw', |
||||
] |
||||
|
||||
class LightningClient extends EventEmitter { |
||||
// These definitions are required for TypeScript. The full list of functions is above, but they are not all used in the code.
|
||||
listpeers?: (...args: any[]) => Promise<any> |
||||
invoice?: (...args: any[]) => Promise<any> |
||||
newaddr?: (...args: any[]) => Promise<any> |
||||
listfunds?: (...args: any[]) => Promise<any> |
||||
getinfo?: (...args: any[]) => Promise<any> |
||||
waitanyinvoice?: (...args: any[]) => Promise<any> |
||||
private rpcPath: string |
||||
private reconnectWait: number |
||||
private reconnectTimeout: NodeJS.Timeout |
||||
private reqcount: number |
||||
private debug |
||||
private client |
||||
private clientConnectionPromise |
||||
|
||||
constructor(rpcPath, debugFlag = false) { |
||||
if (!path.isAbsolute(rpcPath)) { |
||||
throw new Error('The rpcPath must be an absolute path') |
||||
} |
||||
|
||||
rpcPath = path.join(rpcPath, '/bitcoin/lightning-rpc') |
||||
|
||||
debugLightning(`Connecting to ${rpcPath}`) |
||||
|
||||
super() |
||||
this.rpcPath = rpcPath |
||||
this.reconnectWait = 0.5 |
||||
this.reconnectTimeout = null |
||||
this.reqcount = 0 |
||||
|
||||
this.debug = debugFlag |
||||
|
||||
const _self = this |
||||
|
||||
this.client = net.createConnection(rpcPath) |
||||
this.clientConnectionPromise = new Promise(resolve => { |
||||
_self.client.on('connect', () => { |
||||
debug(`Lightning client connected`) |
||||
_self.reconnectWait = 1 |
||||
resolve(true) |
||||
}) |
||||
|
||||
_self.client.on('end', () => { |
||||
_self.increaseWaitTime() |
||||
_self.reconnect() |
||||
}) |
||||
|
||||
_self.client.on('error', error => { |
||||
console.log('Lightning Client Error: ', error) |
||||
_self.increaseWaitTime() |
||||
_self.reconnect() |
||||
}) |
||||
}) |
||||
|
||||
let buffer = Buffer.from('') |
||||
let openCount = 0 |
||||
|
||||
this.client.on('data', data => { |
||||
LightningClient.splitJSON( |
||||
Buffer.concat([buffer, data]), |
||||
buffer.length, |
||||
openCount |
||||
).forEach(partObj => { |
||||
if (partObj.partial) { |
||||
buffer = partObj.string |
||||
openCount = partObj.openCount |
||||
|
||||
return |
||||
} |
||||
|
||||
buffer = Buffer.from('') |
||||
openCount = 0 |
||||
|
||||
try { |
||||
let dataObject = JSON.parse(partObj.string.toString()) |
||||
_self.emit('res:' + dataObject.id, dataObject) |
||||
} catch (err) { |
||||
return |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
static splitJSON(str, startFrom = 0, openCount = 0) { |
||||
const parts = [] |
||||
|
||||
let lastSplit = 0 |
||||
|
||||
for (let i = startFrom; i < str.length; i++) { |
||||
if (i > 0 && str[i - 1] === 115) { |
||||
// 115 => backslash, ignore this character
|
||||
continue |
||||
} |
||||
|
||||
if (str[i] === 123) { |
||||
// '{'
|
||||
openCount++ |
||||
} else if (str[i] === 125) { |
||||
// '}'
|
||||
openCount-- |
||||
|
||||
if (openCount === 0) { |
||||
const start = lastSplit |
||||
const end = i + 1 === str.length ? undefined : i + 1 |
||||
|
||||
parts.push({ |
||||
partial: false, |
||||
string: str.slice(start, end), |
||||
openCount: 0, |
||||
}) |
||||
|
||||
lastSplit = end |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (lastSplit !== undefined) { |
||||
parts.push({ partial: true, string: str.slice(lastSplit), openCount }) |
||||
} |
||||
|
||||
return parts |
||||
} |
||||
|
||||
increaseWaitTime() { |
||||
if (this.reconnectWait >= 16) { |
||||
this.reconnectWait = 16 |
||||
} else { |
||||
this.reconnectWait *= 2 |
||||
} |
||||
} |
||||
|
||||
reconnect() { |
||||
const _self = this |
||||
|
||||
if (this.reconnectTimeout) { |
||||
return |
||||
} |
||||
|
||||
this.reconnectTimeout = setTimeout(() => { |
||||
debugLightning('Trying to reconnect...') |
||||
|
||||
_self.client.connect(_self.rpcPath) |
||||
_self.reconnectTimeout = null |
||||
}, this.reconnectWait * 1000) |
||||
} |
||||
|
||||
call(method, args = []) { |
||||
if (typeof method !== 'string' || !Array.isArray(args)) { |
||||
return Promise.reject(new Error('invalid_call')) |
||||
} |
||||
|
||||
let stackTrace = null |
||||
if (this.debug === true) { |
||||
// not really efficient, we skip this step if debug is not enabled
|
||||
const error = new Error() |
||||
stackTrace = error.stack |
||||
} |
||||
|
||||
const _self = this |
||||
|
||||
const callInt = ++this.reqcount |
||||
const sendObj = { |
||||
method, |
||||
params: args, |
||||
id: callInt.toString(), |
||||
} |
||||
|
||||
// Wait for the client to connect
|
||||
return this.clientConnectionPromise.then( |
||||
() => |
||||
new Promise((resolve, reject) => { |
||||
// Wait for a response
|
||||
this.once('res:' + callInt, response => { |
||||
if (!response.error) { |
||||
resolve(response.result) |
||||
return |
||||
} |
||||
|
||||
reject({ error: response.error, stack: stackTrace }) |
||||
}) |
||||
|
||||
// Send the command
|
||||
_self.client.write(JSON.stringify(sendObj)) |
||||
}) |
||||
) |
||||
} |
||||
} |
||||
|
||||
const protify = s => s.replace(/-([a-z])/g, m => m[1].toUpperCase()) |
||||
|
||||
methods.forEach(k => { |
||||
LightningClient.prototype[protify(k)] = function (...args) { |
||||
return this.call(k, args) |
||||
} |
||||
}) |
||||
|
||||
export default LightningClient |
@ -0,0 +1,237 @@
|
||||
import { v1 as uuidV1 } from 'uuid' |
||||
import express from 'express' |
||||
import events from './events.js' |
||||
import LightningClient from './lightning-client.js' |
||||
import state from './state.js' |
||||
import Client from 'bitcoin-core' |
||||
import chalk from 'chalk' |
||||
import sampleSize from 'lodash.samplesize' |
||||
|
||||
const serverState = state.serverState |
||||
const lightningRouter = express.Router() |
||||
const AO_LIGHTNING_DIR = process.env.AO_LIGHTNING_DIR || process.env.HOME + '/.lightning' |
||||
console.log('AO_LIGHTNING_DIR', AO_LIGHTNING_DIR) |
||||
const client = new LightningClient(AO_LIGHTNING_DIR, true) |
||||
const AO_BITCOIN_NETWORK = process.env.AO_BITCOIN_NETWORK |
||||
const bitClient = new Client({ network: AO_BITCOIN_NETWORK }) |
||||
|
||||
bitClient |
||||
.getBlockchainInfo() |
||||
.then(x => { |
||||
if (x.initialblockdownload) { |
||||
console.log( |
||||
'Initial bitcoin sync detected', |
||||
chalk.red((100 * x.verificationprogress).toFixed(2)), |
||||
'% complete' |
||||
) |
||||
} else { |
||||
console.log(chalk.yellow(x.blocks.toLocaleString()), 'bitcoin blocks') |
||||
} |
||||
}) |
||||
.catch(err => { |
||||
console.log(chalk.red('cannot connect to bitcoind:', err)) |
||||
}) |
||||
|
||||
function getDecode(rawx) { |
||||
return bitClient |
||||
.getRawTransaction(rawx) |
||||
.then(rawTransaction => { |
||||
return bitClient.decodeRawTransaction(rawTransaction) |
||||
}) |
||||
.catch(err => {}) |
||||
} |
||||
|
||||
function newSample() { |
||||
return { super: [], high: [], mid: [], low: [] } |
||||
} |
||||
|
||||
var sampleTxns |
||||
function getMempool() { |
||||
return bitClient.getMempoolInfo().then(memPoolInfo => { |
||||
return bitClient.getRawMempool().then(rawMemPool => { |
||||
sampleTxns = newSample() |
||||
let sample = sampleSize(rawMemPool, 100) |
||||
return sample |
||||
.reduce((prevPromise, txid) => { |
||||
return prevPromise.then(x => { |
||||
return bitClient |
||||
.getMempoolEntry(txid) |
||||
.then(mentry => { |
||||
let satFee = (mentry.fee * 100000000) / mentry.vsize |
||||
if (satFee > 150) { |
||||
sampleTxns.super.push(txid) |
||||
} else if (satFee > 50) { |
||||
sampleTxns.high.push(txid) |
||||
} else if (satFee > 10) { |
||||
sampleTxns.mid.push(txid) |
||||
} else { |
||||
sampleTxns.low.push(txid) |
||||
} |
||||
return Promise.resolve() |
||||
}) |
||||
.catch(noTx => { |
||||
return Promise.resolve() |
||||
}) |
||||
}) |
||||
}, Promise.resolve()) |
||||
.then(x => { |
||||
return bitClient.estimateSmartFee(6).then(smartFee => { |
||||
memPoolInfo.smartFee = smartFee |
||||
memPoolInfo.sampleTxns = sampleTxns.super |
||||
.concat(sampleTxns.high) |
||||
.concat(sampleTxns.med) |
||||
.concat(sampleTxns.low) |
||||
return memPoolInfo |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
lightningRouter.post('/lightning/peer', (req, res) => { |
||||
client |
||||
.listpeers(req.body.pubkey) |
||||
.then(x => { |
||||
res.send(x.peers[0].channels[0]) |
||||
}) |
||||
.catch(err => { |
||||
res.status(400).end() |
||||
}) |
||||
}) |
||||
|
||||
lightningRouter.post('/bitcoin/transaction', (req, res) => { |
||||
console.log('debuggin') |
||||
console.log('req.body', req.body) |
||||
bitClient |
||||
.getMempoolEntry(req.body.txid) |
||||
.then(memPool => { |
||||
getDecode(req.body.txid).then(txn => { |
||||
txn.memPool = memPool |
||||
res.send(txn) |
||||
}) |
||||
}) |
||||
.catch(notInMempool => { |
||||
getDecode(req.body.txid) |
||||
.then(txn => { |
||||
if (txn.vout) { |
||||
try { |
||||
Promise.all( |
||||
txn.vout.map((output, i) => { |
||||
return bitClient.getTxOut(req.body.txid, i) |
||||
}) |
||||
).then(outs => { |
||||
if (outs.some(x => x !== null)) { |
||||
txn.utxo = outs |
||||
} |
||||
res.send(txn) |
||||
}) |
||||
} catch (err) { |
||||
res.status(400).end() |
||||
} |
||||
} |
||||
}) |
||||
.catch(err => { |
||||
res.status(400).end() |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
export function createInvoice(sat, label, description, expiresInSec) { |
||||
return client.invoice(sat * 1000, label, description, expiresInSec) |
||||
} |
||||
|
||||
export function newAddress() { |
||||
return client.newaddr() |
||||
} |
||||
|
||||
function updateAll() { |
||||
checkFunds() |
||||
getInfo() |
||||
} |
||||
|
||||
export function watchOnChain() { |
||||
setInterval(updateAll, 1000 * 60 * 4) |
||||
setTimeout(() => { |
||||
updateAll() |
||||
}, 560) |
||||
} |
||||
|
||||
function checkFunds() { |
||||
return client |
||||
.listfunds() |
||||
.then(result => { |
||||
try { |
||||
result.outputs.forEach(o => { |
||||
if ( |
||||
o.status === 'confirmed' && |
||||
serverState.cash.usedTxIds.indexOf(o.txid) === -1 |
||||
) { |
||||
serverState.tasks.forEach(t => { |
||||
if (t.btcAddr === o.address) { |
||||
events.trigger('task-boosted', { |
||||
taskId: t.taskId, |
||||
amount: o.value, |
||||
txid: o.txid, |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
} catch (err) { |
||||
console.log( |
||||
'lighting error; maybe lightningd (c-lightning) is not running' |
||||
) |
||||
} |
||||
}) |
||||
.catch(err => {}) |
||||
} |
||||
|
||||
function getInfo() { |
||||
return client |
||||
.getinfo() |
||||
.then(mainInfo => { |
||||
bitClient |
||||
.getBlockStats(mainInfo.blockheight) |
||||
.then(blockfo => { |
||||
mainInfo.blockfo = blockfo |
||||
client.listfunds().then(result => { |
||||
mainInfo.channels = result.channels |
||||
mainInfo.outputs = result.outputs |
||||
getMempool().then(mempool => { |
||||
mainInfo.mempool = mempool |
||||
try { |
||||
events.getNodeInfo(mainInfo) |
||||
} catch (err) { |
||||
console.log('getNodeInfo error: ', err) |
||||
} |
||||
}) |
||||
}) |
||||
}) |
||||
.catch(console.log) |
||||
}) |
||||
.catch(console.log) |
||||
} |
||||
|
||||
export function recordEveryInvoice(start) { |
||||
client |
||||
.waitanyinvoice(start) |
||||
.then(invoice => { |
||||
if (!invoice.payment_hash) { |
||||
return console.log('no payment hash wth?', { invoice }) |
||||
} |
||||
serverState.tasks.forEach(t => { |
||||
if (t.payment_hash === invoice.payment_hash) { |
||||
events.trigger('task-boosted-lightning', { |
||||
taskId: t.taskId, |
||||
msatoshi: invoice.msatoshi / 1000, |
||||
payment_hash: invoice.payment_hash, |
||||
pay_index: invoice.pay_index, |
||||
}) |
||||
} |
||||
}) |
||||
recordEveryInvoice(start + 1) // is this recurr broken?
|
||||
}) |
||||
.catch(err => {}) |
||||
} |
||||
|
||||
export default lightningRouter |
@ -0,0 +1,142 @@
|
||||
import cron from 'cron' |
||||
import events from './events.js' |
||||
import state from './state.js' |
||||
const serverState = state.serverState |
||||
import { checkHash, postEvent } from './connector.js' |
||||
import { crawler, crawlerHash } from '../calculations.js' |
||||
import Rsync from 'rsync' |
||||
|
||||
const syncLink = new cron.CronJob({ |
||||
cronTime: '0 */1 * * * *', |
||||
onTick: sync, |
||||
start: false, |
||||
timeZone: 'America/Los_Angeles', |
||||
}) |
||||
|
||||
let rsync |
||||
|
||||
function sync() { |
||||
console.log('sync trig') |
||||
/*serverState.ao.forEach(a => { |
||||
a.links.forEach(l => { |
||||
let crawlered = crawler(serverState.tasks, l) |
||||
let expectedHash = crawlerHash(serverState.tasks, l) |
||||
checkHash( |
||||
a.address, |
||||
a.outboundSecret, |
||||
l, |
||||
hashRes => { |
||||
console.log( |
||||
`expectedHash: ${expectedHash}, hashRes: ${ |
||||
hashRes.length <= 64 ? hashRes : 'Failed' |
||||
}` |
||||
) |
||||
if (expectedHash !== hashRes) { |
||||
const tasksToSend = getList(crawlered) |
||||
postEvent( |
||||
a.address, |
||||
a.outboundSecret, |
||||
{ |
||||
type: 'tasks-received', |
||||
tasks: tasksToSend, |
||||
}, |
||||
connectorRes => { |
||||
console.log( |
||||
'ao relay response:', |
||||
connectorRes?.statusCode === 200 |
||||
? 'sent cards' |
||||
: connectorRes?.statusCode |
||||
) |
||||
if (rsync) { |
||||
console.log('memes are still syncing from last sync trig...') |
||||
} else { |
||||
console.log('checking for memes to sync...') |
||||
const memes = serverState.memes.filter(meme => { |
||||
const result = tasksToSend.some( |
||||
task => task.taskId === meme.memeId |
||||
) |
||||
if (result) |
||||
console.log('Found meme! Meme is', meme.filename) |
||||
return result |
||||
}) |
||||
if (memes?.length >= 1) { |
||||
console.log('starting meme sync for', memes.length, 'memes') |
||||
const filenames = memes.map( |
||||
meme => process.env.AO_MEMES_DIR + '/' + meme.filename |
||||
) |
||||
console.log('filenames:', filenames) |
||||
|
||||
// Build the command
|
||||
rsync = new Rsync() |
||||
.output( |
||||
function (data) { |
||||
// do things like parse progress
|
||||
}, |
||||
function (data) { |
||||
// do things like parse error output
|
||||
console.log('error output: ', data) |
||||
} |
||||
) |
||||
// .shell('ssh')
|
||||
.executable( |
||||
'torsocks rsync --ignore-existing --chmod=664 -vvutz -e "ssh -i ' + |
||||
process.env.AO_MEMES_SSH_KEY_PATH + |
||||
'"' |
||||
) |
||||
// .shell('sh')
|
||||
// .flags('-vvuntz')
|
||||
// .flags('--compress')
|
||||
// .flags('--upda')
|
||||
// .flags('--ignore-existing')
|
||||
// .flags('--times')
|
||||
// .flags('--chmod=664')
|
||||
// .flags('--dry-run')
|
||||
// .flags('--checksum')
|
||||
.source(filenames) |
||||
.destination( |
||||
process.env.AO_MEMES_SSH_USERNAME + |
||||
'@' + |
||||
a.address + |
||||
':/home/' + |
||||
process.env.AO_MEMES_SSH_USERNAME + |
||||
'/.ao/memes/' |
||||
) |
||||
|
||||
console.log('rsync command is ', rsync.command()) |
||||
|
||||
// Execute the command
|
||||
rsync.execute(function (error, code, cmd) { |
||||
// we're done
|
||||
if (error) { |
||||
console.log('Error syncing memes: ', error) |
||||
return |
||||
} |
||||
console.log( |
||||
'synced', |
||||
memes.length, |
||||
'memes! code is ', |
||||
code, |
||||
'and cmd is', |
||||
cmd |
||||
) |
||||
}) |
||||
} |
||||
} |
||||
}, |
||||
false |
||||
) |
||||
} |
||||
}, |
||||
false |
||||
) |
||||
}) |
||||
})*/ |
||||
} |
||||
|
||||
function getList(taskIds) { |
||||
return serverState.tasks.filter(t => taskIds.indexOf(t.taskId) > -1) |
||||
} |
||||
|
||||
export default function () { |
||||
syncLink.start() |
||||
} |
@ -0,0 +1,96 @@
|
||||
import { getResource } from './utils.js' |
||||
import events from './events.js' |
||||
import state from './state.js' |
||||
const serverState = state.serverState |
||||
// const lightning from ./lightning')
|
||||
|
||||
function checkForChargedEvent(resourceId) { |
||||
let charged |
||||
serverState.bookings.forEach(b => { |
||||
if (resourceId === b.resourceId) { |
||||
let dnow = new Date() |
||||
let now = dnow.getTime() |
||||
let tsUntilStart = parseInt(b.startTs) - now |
||||
let tsUntilEnd = parseInt(b.endTs) - now |
||||
let current = tsUntilStart < 0 && tsUntilEnd > 0 |
||||
if (current && b.charge > 0) { |
||||
charged = b.charge |
||||
} |
||||
} |
||||
}) |
||||
return charged |
||||
} |
||||
|
||||
function reactions(ev) { |
||||
process.nextTick(err => { |
||||
switch (ev.type) { |
||||
case 'task-boosted': |
||||
case 'task-boosted-lightning': |
||||
let optionList = [] |
||||
let defaultPrice |
||||
let resourceId |
||||
let resourceList = serverState.resources.map(r => r.resourceId) |
||||
let amount = parseFloat(ev.amount) |
||||
|
||||
serverState.tasks.some(t => { |
||||
if ( |
||||
resourceList.indexOf(t.taskId) > -1 && |
||||
t.priorities.indexOf(ev.taskId) > -1 |
||||
) { |
||||
resourceId = t.taskId |
||||
return true |
||||
} |
||||
}) |
||||
if (resourceId) { |
||||
console.log('got resourceId, attempting trigger', resourceId) |
||||
serverState.resources.some(r => { |
||||
if (r.resourceId === resourceId) { |
||||
defaultPrice = r.charge |
||||
return true |
||||
} |
||||
}) |
||||
serverState.tasks.some(t => { |
||||
if (ev.taskId === t.taskId) { |
||||
let str = t.name |
||||
let cashTagLocation = str.search(/\$/) |
||||
let customPrice = parseFloat( |
||||
str.slice(cashTagLocation + 1, cashTagLocation + 5) |
||||
) |
||||
if (customPrice > 0) { |
||||
console.log('using custom price, ', customPrice) |
||||
defaultPrice = customPrice |
||||
} |
||||
let hopper = t.name.split(':')[0] |
||||
events.trigger( |
||||
'resource-used', |
||||
{ |
||||
resourceId: resourceId, |
||||
memberId: '', |
||||
amount: defaultPrice, |
||||
charged: hopper, |
||||
}, |
||||
console.log |
||||
) |
||||
return true |
||||
} |
||||
}) |
||||
} |
||||
break |
||||
case 'member-field-updated': |
||||
break |
||||
case 'member-paid': |
||||
break |
||||
case 'resource-stocked': |
||||
events.trigger('member-activated', { memberId: ev.memberId }) |
||||
break |
||||
case 'member-address-updated': |
||||
break |
||||
case 'member-created': |
||||
break |
||||
case 'resource-created': |
||||
break |
||||
} |
||||
}) |
||||
} |
||||
|
||||
export default reactions |
@ -0,0 +1,89 @@
|
||||
import cron from 'cron' |
||||
import events from './events.js' |
||||
import state from './state.js' |
||||
const serverState = state.serverState |
||||
|
||||
const rentJob = new cron.CronJob({ |
||||
cronTime: '0 0 0 1 * *', |
||||
onTick: rent, |
||||
start: false, |
||||
timeZone: 'America/Los_Angeles', |
||||
}) |
||||
|
||||
const deactivateJob = new cron.CronJob({ |
||||
cronTime: '11 11 11 * * 0', |
||||
onTick: deactivate, |
||||
start: false, |
||||
timeZone: 'America/Los_Angeles', |
||||
}) |
||||
|
||||
const fundJob = new cron.CronJob({ |
||||
cronTime: '12 12 12 * * 0', |
||||
onTick: fundGuilds, |
||||
start: false, |
||||
timeZone: 'America/Los_Angeles', |
||||
}) |
||||
|
||||
function rent() { |
||||
let activeMembers = serverState.members.filter(m => { |
||||
return m.active > 0 |
||||
}) |
||||
let fixed = serverState.cash.rent |
||||
let numActiveMembers = activeMembers.length |
||||
let perMonth = fixed / numActiveMembers |
||||
let charged = Math.min(perMonth, serverState.cash.cap) |
||||
let notes = '' |
||||
|
||||
activeMembers.forEach(m => { |
||||
events.trigger( |
||||
'member-charged', |
||||
{ memberId: m.memberId, charged, notes }, |
||||
null |
||||
) |
||||
}) |
||||
} |
||||
|
||||
function deactivate() { |
||||
serverState.tasks.forEach(t => { |
||||
const m = serverState.members.find(member => member.memberId === t.taskId) |
||||
if (!m) return |
||||
if (t.boost <= 0 && m.active > 0) { |
||||
events.trigger('member-deactivated', { memberId: t.taskId }, null) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
function fundGuilds() { |
||||
let activeMembers = {} |
||||
serverState.members.forEach(m => { |
||||
if (m.active > 0) { |
||||
activeMembers[m.memberId] = true |
||||
} |
||||
}) |
||||
if (Object.keys(activeMembers).length >= 1) { |
||||
const loadedMultiplier = parseInt(process.env.AO_JUBILEE_MULTIPLIER) |
||||
const multiplier = loadedMultiplier > 0 ? loadedMultiplier : 10 |
||||
serverState.tasks.forEach(task => { |
||||
if (task.guild) { |
||||
let hodlrsNoDuplicates = [...task?.deck] |
||||
const totalCurrentEndorsements = hodlrsNoDuplicates?.length || 0 |
||||
if (totalCurrentEndorsements > 0) { |
||||
events.trigger( |
||||
'task-boosted', |
||||
{ |
||||
taskId: task.taskId, |
||||
amount: totalCurrentEndorsements * multiplier, |
||||
}, |
||||
null |
||||
) |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
export default function () { |
||||
rentJob.start() |
||||
// deactivateJob.start()
|
||||
fundJob.start() |
||||
} |
@ -0,0 +1,775 @@
|
||||
import express from 'express' |
||||
import path from 'path' |
||||
import cookieParser from 'cookie-parser' |
||||
import state from './state.js' |
||||
import spec from './spec.js' |
||||
import fobtap from './fobtap.js' |
||||
import { serverAuth } from './auth.js' |
||||
import lightningRouter from './lightning.js' |
||||
import fs from 'fs' |
||||
import multer from 'multer' |
||||
import { Task } from '../types.js' |
||||
|
||||
import { addMeme } from './files.js' |
||||
import events from './events.js' |
||||
|
||||
import { crawlerHash } from '../calculations.js' |
||||
import validators from './validators.js' |
||||
|
||||
import { fileURLToPath } from 'url' |
||||
import util from 'util' |
||||
|
||||
import { allReachableHeldParentsServer } from '../cards.js' |
||||
|
||||
const __filename = fileURLToPath(import.meta.url) |
||||
const __dirname = path.dirname(__filename) |
||||
|
||||
const AO_MEMES_DIR = process.env.MEMES_DIR || process.env.HOME + '/.ao/memes' |
||||
|
||||
export default function applyRouter(app) { |
||||
var myLogger = function (req, res, next) { |
||||
// console.log('AO: server/router.js: myLogger: ', {"url": req.url, "body": req.body})
|
||||
next() |
||||
} |
||||
app.use(myLogger) |
||||
|
||||
app.use(express.static(path.join(__dirname, '../../dist'))) |
||||
app.use(express.static(path.join(__dirname, '../../public'))) |
||||
app.get('/public/manifest.json', (req, res) => { |
||||
res.sendFile(path.join(__dirname, '../../dist/public/manifest.json')) |
||||
}) |
||||
app.get('/public/favicon.ico', (req, res) => { |
||||
res.sendFile(path.join(__dirname, '../../dist/public/favicon.ico')) |
||||
}) |
||||
app.use(express.json({ limit: '100mb' })) |
||||
app.use(cookieParser()) |
||||
app.use( |
||||
express.urlencoded({ |
||||
extended: true, |
||||
limit: '1000mb', |
||||
}) |
||||
) |
||||
|
||||
// app.use('/memes', express.static(process.env.AO_MEMES_DIR))
|
||||
|
||||
// Returns a list of all the tor addresses this AO knows about
|
||||
// todo: Next upgrade will make a distinction between bootstrapping addresses and private connections
|
||||
app.get('/bootstrap', (req, res) => { |
||||
console.log('/bootstrap', state.serverState.ao) |
||||
const onions = state.serverState.ao.map(ao => ao.address) |
||||
res.json({ addresses: onions }) |
||||
}) |
||||
|
||||
app.use(serverAuth) // below here requires auth token
|
||||
|
||||
app.use(spec) // handles event creation
|
||||
app.use(fobtap) // handles rfid scan devices
|
||||
app.use(lightningRouter) |
||||
|
||||
app.post('/logout', (req, res) => { |
||||
console.log('Logging out and clearing token (new way)') |
||||
res.clearCookie('token') |
||||
console.log('sending end() res') |
||||
res.end() |
||||
}) |
||||
|
||||
app.post('/state', (req, res) => { |
||||
debugger |
||||
|
||||
let reqOwner = req.reqOwner |
||||
|
||||
let stateToSend |
||||
stateToSend = { tasks: [] } |
||||
|
||||
let dataPackageToSendToClient = { stateToSend, metaData: undefined } |
||||
|
||||
let memberDeckSize = 0 |
||||
|
||||
// Check that the member has an existing member card
|
||||
let foundMemberCard = false |
||||
console.log('reqOwner is:', reqOwner) |
||||
state.pubState.tasks.forEach(taskItem => { |
||||
if ( |
||||
taskItem.taskId.trim().toLowerCase() === reqOwner.trim().toLowerCase() |
||||
) { |
||||
console.log('found member card for reqOwner:', taskItem) |
||||
foundMemberCard = true |
||||
} |
||||
}) |
||||
if (!foundMemberCard) { |
||||
console.log('Missing member card for reqOwner:', reqOwner) |
||||
} |
||||
|
||||
// Include all member cards (name equals taskId)
|
||||
// Include the community hub card itself
|
||||
for (let [key, value] of Object.entries(state.pubState)) { |
||||
if (key !== 'tasks') { |
||||
stateToSend[key] = state.pubState[key] |
||||
} else { |
||||
for (let taskItem of value as Task[]) { |
||||
if ( |
||||
taskItem.name === taskItem.taskId || |
||||
taskItem.name === 'community hub' |
||||
) { |
||||
if (taskItem.name === reqOwner) { |
||||
console.log('Adding member card for reqOwner', reqOwner) |
||||
} |
||||
stateToSend.tasks.push(taskItem) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Include their bookmarks card itself
|
||||
let bookmarksTaskId |
||||
let pins = [] |
||||
state.pubState.tasks.forEach(taskItem => { |
||||
if (taskItem.deck && taskItem.deck.indexOf(reqOwner) !== -1) |
||||
memberDeckSize++ |
||||
if (taskItem.name === reqOwner + '-bookmarks') { |
||||
bookmarksTaskId = taskItem.taskId |
||||
stateToSend.tasks.push(taskItem) |
||||
pins = taskItem.pins |
||||
} |
||||
}) |
||||
|
||||
// Include cards on their bookmarks bar
|
||||
state.pubState.tasks.forEach(taskItem => { |
||||
if (pins.some(pin => taskItem.taskId === pin.taskId)) { |
||||
stateToSend.tasks.push(taskItem) |
||||
} |
||||
}) |
||||
|
||||
// Include cards passed to them as a gift
|
||||
state.pubState.tasks.forEach(taskItem => { |
||||
if (taskItem.passed.some(pass => pass[1] === reqOwner)) { |
||||
stateToSend.tasks.push(taskItem) |
||||
} |
||||
}) |
||||
|
||||
// Include guilds they are holding
|
||||
state.pubState.tasks.forEach(taskItem => { |
||||
if ( |
||||
taskItem.guild && |
||||
taskItem.guild.length >= 1 && |
||||
taskItem.deck.includes(reqOwner) |
||||
) { |
||||
stateToSend.tasks.push(taskItem) |
||||
} |
||||
}) |
||||
|
||||
// Also include the first priority of every card we are sending
|
||||
let priorityIdList = [] |
||||
stateToSend.tasks.forEach(taskItem => { |
||||
if (taskItem?.priorities?.length) { |
||||
priorityIdList.push(taskItem.priorities[taskItem.priorities.length - 1]) |
||||
} |
||||
}) |
||||
let priorityTaskItems = [] |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (priorityIdList.includes(taskItem.taskId)) { |
||||
priorityTaskItems.push(taskItem) // will add duplicates
|
||||
priorityIdList.splice(priorityIdList.indexOf(taskItem.taskId), 1) |
||||
if (priorityIdList.length === 0) { |
||||
return true |
||||
} |
||||
} |
||||
}) |
||||
|
||||
// Also include all the cards stashed in the cards we are sending Todo: only send cards they have high enough level to see
|
||||
stateToSend.tasks.forEach(taskItem => { |
||||
if (taskItem.stash) { |
||||
Object.entries<string[]>(taskItem.stash).forEach(levelAndTaskIds => { |
||||
let [level, tIds]: [string, string[]] = levelAndTaskIds |
||||
tIds.forEach(tId => { |
||||
const foundTask = state.pubState.tasks.find(t => t.taskId === tId) |
||||
if (foundTask) { |
||||
stateToSend.tasks.push(foundTask) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
}) |
||||
|
||||
// Also include everyone holding any of the cards we are sending
|
||||
/*let holderIdList = [] |
||||
stateToSend.tasks.forEach(taskItem => { |
||||
if (taskItem?.deck?.length) { |
||||
holderIdList.push(taskItem.deck) |
||||
} |
||||
}) |
||||
let holderTaskItems = [] |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (holderIdList.includes(taskItem.taskId)) { |
||||
holderTaskItems.push(taskItem) // will add duplicates
|
||||
holderIdList.splice(holderIdList.indexOf(taskItem.taskId), 1) |
||||
if (holderIdList.length === 0) { |
||||
return true |
||||
} |
||||
} |
||||
})*/ |
||||
|
||||
// Also include any events before the next three days (including past events), whether or not they are holding them
|
||||
const msNow = Date.now() |
||||
const timeRangeToSend = 1000 * 60 * 60 * 24 * 3 |
||||
state.pubState.tasks.forEach(taskItem => { |
||||
if ( |
||||
taskItem.book && |
||||
taskItem.book.startTs >= 1 && |
||||
taskItem.book.startTs <= msNow + timeRangeToSend |
||||
) { |
||||
stateToSend.tasks.push(taskItem) |
||||
} |
||||
}) |
||||
|
||||
// Remove duplicates and combine lists
|
||||
stateToSend.tasks = [ |
||||
...new Set([...stateToSend.tasks, ...priorityTaskItems]), |
||||
] |
||||
|
||||
// Include all parent cards of the cards we are sending using this somewhat slow algorithm
|
||||
let heldParentTasks = [] |
||||
stateToSend.tasks.forEach(taskItem => { |
||||
heldParentTasks = heldParentTasks.concat( |
||||
allReachableHeldParentsServer(state.pubState.tasks, taskItem, reqOwner) |
||||
) |
||||
}) |
||||
|
||||
// Remove duplicates and combine lists again
|
||||
stateToSend.tasks = [...new Set([...stateToSend.tasks, ...heldParentTasks])] |
||||
|
||||
dataPackageToSendToClient.metaData = { memberDeckSize, bookmarksTaskId } |
||||
|
||||
if ( |
||||
stateToSend.tasks.some( |
||||
taskItem => |
||||
taskItem.taskId.trim().toLowerCase() === reqOwner.trim().toLowerCase() |
||||
) |
||||
) { |
||||
console.log('Member card is included in outgoing state cards.') |
||||
} else { |
||||
console.log( |
||||
'Member card is missing from outgoing state cards for memberId', |
||||
reqOwner |
||||
) |
||||
} |
||||
|
||||
console.log('POST stateToSend.tasks.length is', stateToSend.tasks.length) |
||||
res.json(dataPackageToSendToClient) |
||||
}) |
||||
|
||||
app.post('/members', (req, res) => { |
||||
res.json(state.pubState.members) |
||||
}) |
||||
|
||||
app.post('/fetchTasks', (req, res) => { |
||||
let errRes = [] |
||||
let foundTasks |
||||
|
||||
console.log('res text is', res.text) |
||||
console.log('AO: server/router.js: fetchTaskByID: ', req.body) |
||||
|
||||
let taskIdList = req.body.taskIds |
||||
if (!Array.isArray(taskIdList)) { |
||||
console.log('/fetchTasks only accepts an array of taskIds in JSON parameter named taskIds, request failed') |
||||
res.status(400).send({ success: false, errorList: errRes }) |
||||
return false |
||||
} |
||||
|
||||
let allTaskIdsAreSane = true |
||||
taskIdList.some(taskId => { |
||||
if (!validators.isTaskId_sane(taskId, errRes)) { |
||||
console.log('Not all requested task IDs are sane:', taskId) |
||||
allTaskIdsAreSane = false |
||||
return true |
||||
} |
||||
}) |
||||
|
||||
let foundThisTaskList = [] |
||||
let foundAllTaskItems = false |
||||
if (allTaskIdsAreSane === true) { |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (taskIdList.includes(taskItem.taskId)) { |
||||
foundThisTaskList.push(taskItem) |
||||
taskIdList.splice(taskIdList.indexOf(taskItem.taskId), 1) |
||||
if (taskIdList.length === 0) { |
||||
foundAllTaskItems = true |
||||
return true |
||||
} |
||||
} |
||||
}) |
||||
|
||||
if (foundThisTaskList.length === 0) { |
||||
res.status(400).send({ success: false, errorList: errRes }) |
||||
} else { |
||||
res.status(200).json(foundThisTaskList) |
||||
} |
||||
} |
||||
}) |
||||
|
||||
app.post('/fetchTaskByID', (req, res) => { |
||||
let errRes = [] |
||||
let foundThisTask |
||||
|
||||
let taskIdList = req.body.taskId |
||||
let taskIdListParameterWasSingleValue = false |
||||
if (!Array.isArray(taskIdList)) { |
||||
taskIdList = [taskIdList] |
||||
taskIdListParameterWasSingleValue = true |
||||
} |
||||
|
||||
let allTaskIdsAreSane = true |
||||
taskIdList.some(taskId => { |
||||
if (!validators.isTaskId_sane(taskId, errRes)) { |
||||
console.log('Not all requested task IDs are sane:', taskId) |
||||
allTaskIdsAreSane = false |
||||
return true |
||||
} |
||||
}) |
||||
|
||||
let foundThisTaskList = [] |
||||
let foundAllTaskItems = false |
||||
if (allTaskIdsAreSane === true) { |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (taskIdList.includes(taskItem.taskId)) { |
||||
foundThisTaskList.push(taskItem) |
||||
taskIdList.splice(taskIdList.indexOf(taskItem.taskId), 1) |
||||
if (taskIdList.length === 0) { |
||||
foundAllTaskItems = true |
||||
return true |
||||
} |
||||
} |
||||
}) |
||||
|
||||
// Also return the first priority for each card we are returning, since priorities show up prior to the card in priority mode
|
||||
let priorityIdList = [] |
||||
foundThisTaskList.forEach(foundTask => { |
||||
if (foundTask?.priorities?.length) { |
||||
priorityIdList.push( |
||||
foundTask.priorities[foundTask.priorities.length - 1] |
||||
) |
||||
} |
||||
}) |
||||
let foundAllPriorityItems = priorityIdList.length <= 0 |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (priorityIdList.includes(taskItem.taskId)) { |
||||
foundThisTaskList.push(taskItem) // will add duplicates
|
||||
priorityIdList.splice(priorityIdList.indexOf(taskItem.taskId), 1) |
||||
if (priorityIdList.length === 0) { |
||||
foundAllPriorityItems = true |
||||
return true |
||||
} |
||||
} |
||||
}) |
||||
foundAllTaskItems = foundAllTaskItems && foundAllPriorityItems |
||||
|
||||
// Also return all the member cards of members who are holding this card
|
||||
let holderIdList = [] |
||||
foundThisTaskList.forEach(foundTask => { |
||||
if (foundTask?.deck?.length) { |
||||
holderIdList.push(foundTask.deck) |
||||
} |
||||
}) |
||||
let foundAllHolderItems = holderIdList.length <= 0 |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (holderIdList.includes(taskItem.taskId)) { |
||||
foundThisTaskList.push(taskItem) // will add duplicates
|
||||
holderIdList.splice(holderIdList.indexOf(taskItem.taskId), 1) |
||||
if (holderIdList.length === 0) { |
||||
foundAllHolderItems = true |
||||
return true |
||||
} |
||||
} |
||||
}) |
||||
foundAllTaskItems = foundAllTaskItems && foundAllHolderItems |
||||
|
||||
// Also return all parent cards held by this member reachable through a continuous path
|
||||
let heldParentTasks = [] |
||||
foundThisTaskList.forEach(taskItem => { |
||||
heldParentTasks = heldParentTasks.concat( |
||||
allReachableHeldParentsServer( |
||||
state.pubState.tasks, |
||||
taskItem, |
||||
req.reqOwner |
||||
) |
||||
) |
||||
}) |
||||
|
||||
// Also return all stashed cards within this card Todo: Only send cards the member has access to
|
||||
let stashedTasks = [] |
||||
foundThisTaskList.forEach(taskItem => { |
||||
if (taskItem.stash) { |
||||
Object.entries<string[]>(taskItem.stash).forEach(levelAndTaskIds => { |
||||
let [level, tIds] = levelAndTaskIds |
||||
tIds.forEach(tId => { |
||||
const foundTask = state.pubState.tasks.find(t => t.taskId === tId) |
||||
if (foundTask) { |
||||
stashedTasks.push(foundTask) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
}) |
||||
|
||||
// Remove duplicates and combine tasks into one list
|
||||
foundThisTaskList = [ |
||||
...new Set([...foundThisTaskList, ...heldParentTasks, ...stashedTasks]), |
||||
] |
||||
|
||||
let objectToSend |
||||
if (taskIdListParameterWasSingleValue === true) { |
||||
if (foundThisTaskList.length === 0) { |
||||
res.status(400).send({ success: false, errorList: errRes }) |
||||
} else { |
||||
res.status(200).json(foundThisTaskList[0]) |
||||
} |
||||
} else { |
||||
res.status(200).json({ foundThisTaskList, foundAllTaskItems }) |
||||
} |
||||
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// errRes.push("AO: server/router.js: fetchTaskByID: task not found ", { "req.body": req.body, foundThisTask});
|
||||
// res.status(400).send({ "success": false, "errorList": errRes });
|
||||
// }
|
||||
} else { |
||||
// console.log("AO: server/router.js: fetchTaskByID: invalid taskId found in list: ", taskIdList);
|
||||
res.status(400).send(errRes) |
||||
} |
||||
}) |
||||
|
||||
app.post('/fetchDeck', (req, res) => { |
||||
let errRes = [] |
||||
|
||||
let foundThisTaskList = state.pubState.tasks.filter(taskItem => { |
||||
return taskItem.deck.includes(req.reqOwner) |
||||
}) |
||||
|
||||
// Also return the first priority for each card we are returning, since priorities show up prior to the card in priority mode
|
||||
let priorityIdList = [] |
||||
foundThisTaskList.forEach(foundTask => { |
||||
if (foundTask?.priorities?.length) { |
||||
priorityIdList.push( |
||||
foundTask.priorities[foundTask.priorities.length - 1] |
||||
) |
||||
} |
||||
}) |
||||
let foundAllPriorityItems = priorityIdList.length <= 0 |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (priorityIdList.includes(taskItem.taskId)) { |
||||
foundThisTaskList.push(taskItem) // will add duplicates
|
||||
priorityIdList.splice(priorityIdList.indexOf(taskItem.taskId), 1) |
||||
if (priorityIdList.length === 0) { |
||||
foundAllPriorityItems = true |
||||
return true |
||||
} |
||||
} |
||||
}) |
||||
let foundAllTaskItems = foundAllPriorityItems |
||||
|
||||
// Also return all the member cards of members who are holding this card
|
||||
let holderIdList = [] |
||||
foundThisTaskList.forEach(foundTask => { |
||||
if (foundTask?.deck?.length) { |
||||
holderIdList.push(foundTask.deck) |
||||
} |
||||
}) |
||||
let foundAllHolderItems = holderIdList.length <= 0 |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (holderIdList.includes(taskItem.taskId)) { |
||||
foundThisTaskList.push(taskItem) // will add duplicates
|
||||
holderIdList.splice(holderIdList.indexOf(taskItem.taskId), 1) |
||||
if (holderIdList.length === 0) { |
||||
foundAllHolderItems = true |
||||
return true |
||||
} |
||||
} |
||||
}) |
||||
foundAllTaskItems = foundAllTaskItems && foundAllHolderItems |
||||
|
||||
// Remove duplicates
|
||||
foundThisTaskList = [...new Set([...foundThisTaskList])] |
||||
|
||||
// Remove broken cards
|
||||
let brokenCards = 0 |
||||
foundThisTaskList = foundThisTaskList.filter(taskItem => { |
||||
if (!Array.isArray(taskItem.passed)) { |
||||
brokenCards++ |
||||
return false |
||||
} |
||||
return true |
||||
}) |
||||
console.log( |
||||
'Sending entire deck of', |
||||
foundThisTaskList.length, |
||||
'except', |
||||
brokenCards, |
||||
'broken cards to member', |
||||
req.reqOwner |
||||
) |
||||
|
||||
let objectToSend |
||||
if (foundThisTaskList.length === 0) { |
||||
res.status(400).send({ success: false, errorList: errRes }) |
||||
} else { |
||||
res.status(200).json({ foundThisTaskList }) |
||||
} |
||||
}) |
||||
|
||||
app.post('/fetchTaskByName_exact', (req, res) => { |
||||
console.log("/fetchTaskByName_exact") |
||||
let errRes = [] |
||||
let foundThisTask |
||||
|
||||
const trimmedTaskName = req.body.taskName.trim().toLowerCase() |
||||
if (validators.isTaskName_sane(req.body.taskName, errRes)) { |
||||
let taskName = req.body.taskName |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (taskItem.name.toLowerCase() === trimmedTaskName) { |
||||
foundThisTask = taskItem |
||||
return true |
||||
} |
||||
}) |
||||
|
||||
if (foundThisTask) { |
||||
// console.log("AO: server/router.js: fetchTaskByName: task found: ", {"taskName": req.body.taskName, "result": foundThisTask})
|
||||
res.status(200).send(foundThisTask) |
||||
} else { |
||||
console.log('AO: server/router.js: fetchTaskByName_exact: task not found ', { |
||||
'req.body': req.body, |
||||
foundThisTask, |
||||
}) |
||||
errRes.push('task name not found') |
||||
res.status(204).send({ success: false, errorList: errRes }) |
||||
} |
||||
} else { |
||||
// console.log("AO: server/router.js: fetchTaskByName: invalid taskName: ", { "req.body": req.body, foundThisTask } )
|
||||
res.status(400).send(errRes) |
||||
} |
||||
}) |
||||
|
||||
app.post('/fetchTaskByName_exact', (req, res) => { |
||||
console.log("/fetchTaskByName") |
||||
let errRes = [] |
||||
let foundThisTask |
||||
|
||||
const trimmedTaskName = req.body.taskName.trim().toLowerCase() |
||||
if (validators.isTaskName_sane(req.body.taskName, errRes)) { |
||||
let taskName = req.body.taskName |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (taskItem.name.toLowerCase() === trimmedTaskName) { |
||||
foundThisTask = taskItem |
||||
return true |
||||
} |
||||
}) |
||||
|
||||
if (foundThisTask) { |
||||
let foundThisTaskList = [foundThisTask] |
||||
|
||||
// Also return the first priority for each card we are returning, since priorities show up prior to the card in priority mode
|
||||
const firstPriorityId = foundThisTask?.priorities?.length |
||||
? foundThisTask.priorities[foundThisTask.priorities.length - 1] |
||||
: null |
||||
let foundPriority |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (taskItem.taskId === firstPriorityId) { |
||||
foundThisTaskList.push(taskItem) |
||||
return true |
||||
} |
||||
}) |
||||
|
||||
// Also return the pins and the first subTask of each color so they show up right away? Or make it so piles load asynchronously
|
||||
|
||||
// Also return all the member cards of members who are holding this card
|
||||
let holderIdList = [] |
||||
if (foundThisTask?.deck?.length) { |
||||
holderIdList.push(foundThisTask.deck) |
||||
} |
||||
let foundAllHolderItems = holderIdList.length <= 0 |
||||
state.pubState.tasks.some(taskItem => { |
||||
if (holderIdList.includes(taskItem.taskId)) { |
||||
foundThisTaskList.push(taskItem) // will add duplicates
|
||||
holderIdList.splice(holderIdList.indexOf(taskItem.taskId), 1) |
||||
if (holderIdList.length === 0) { |
||||
foundAllHolderItems = true |
||||
return true |
||||
} |
||||
} |
||||
}) |
||||
|
||||
// Remove duplicates
|
||||
foundThisTaskList = [...new Set(foundThisTaskList)] |
||||
|
||||
// console.log("AO: server/router.js: fetchTaskByName: task found: ", {"taskName": req.body.taskName, "result": foundThisTask})
|
||||
res.status(200).send({ |
||||
foundThisTaskList, |
||||
foundAllTaskItems: |
||||
!firstPriorityId || (firstPriorityId && foundPriority), |
||||
}) |
||||
} else { |
||||
console.log('AO: server/router.js: fetchTaskByName: task not found ', { |
||||
'req.body': req.body, |
||||
foundThisTask, |
||||
}) |
||||
errRes.push('task name not found') |
||||
res.status(204).send({ success: false, errorList: errRes }) |
||||
} |
||||
} else { |
||||
// console.log("AO: server/router.js: fetchTaskByName: invalid taskName: ", { "req.body": req.body, foundThisTask } )
|
||||
res.status(400).send(errRes) |
||||
} |
||||
}) |
||||
|
||||
const handleError = (err, res) => { |
||||
res.status(500).contentType('text/plain').end('Oops! Something went wrong!') |
||||
} |
||||
|
||||
const upload = multer({ |
||||
dest: path.join(AO_MEMES_DIR, '/.temp'), |
||||
// you might also want to set some limits: https://github.com/expressjs/multer#limits
|
||||
}) |
||||
|
||||
const uploadFormName = 'file' |
||||
|
||||
app.post('/upload', upload.single(uploadFormName), (req, res) => { |
||||
// if (err) {
|
||||
// console.log('UPLOAD ERROR: ', err)
|
||||
// res.status(400).send([])
|
||||
// return
|
||||
// }
|
||||
const tempPath = req.file.path |
||||
const targetPath = path.join( |
||||
AO_MEMES_DIR, |
||||
req.file.originalname |
||||
) |
||||
console.log('originalname is ', req.file.originalname) |
||||
console.log('temppath is ', tempPath) |
||||
console.log('targepath is ', targetPath) |
||||
// console.log('req is ', req)
|
||||
|
||||
fs.rename(tempPath, targetPath, err => { |
||||
// if (err) return handleError(err, res)
|
||||
const memePromise = addMeme(req.file.originalname, targetPath) |
||||
console.log('memePromise is ', memePromise) |
||||
memePromise.then(newTaskId => { |
||||
console.log('returned. newTaskId is', newTaskId) |
||||
if (newTaskId) { |
||||
res.status(200).send(newTaskId) |
||||
} else { |
||||
res.status(400).send([]) |
||||
} |
||||
}) |
||||
// .catch(res.status(400).send([]))
|
||||
}) |
||||
}) |
||||
|
||||
app.get('/meme/:memeHash', (req, res) => { |
||||
console.log('meme route detected, hash is ', req.params.memeHash) |
||||
const meme = state.serverState.memes.find(meme => { |
||||
return meme.hash === req.params.memeHash |
||||
}) |
||||
if (!meme || !meme.filename) { |
||||
res.status(604) |
||||
return |
||||
} |
||||
const memePath = path.join(AO_MEMES_DIR, meme.filename) |
||||
console.log('meme path is ', memePath) |
||||
// res.contentType(memePath)
|
||||
res |
||||
.set('Cache-Control', 'public,max-age=31536000,immutable') |
||||
.sendFile(memePath) |
||||
}) |
||||
|
||||
app.get('/download/:memeHash', (req, res) => { |
||||
console.log('download route detected, hash is ', req.params.memeHash) |
||||
const meme = state.serverState.memes.find(meme => { |
||||
return meme.hash === req.params.memeHash |
||||
}) |
||||
const memePath = path.join(AO_MEMES_DIR, meme.filename) |
||||
console.log('meme path is ', memePath) |
||||
// res.contentType(memePath)
|
||||
res.download(memePath) |
||||
}) |
||||
|
||||
app.post('/taskhash/:taskId', (req, res) => { |
||||
res.end(crawlerHash(state.serverState.tasks, req.params.taskId)) |
||||
}) |
||||
|
||||
app.post('/search/:query', (req, res) => { |
||||
const search = decodeURIComponent(req.params.query) |
||||
const { take, skip } = req.query |
||||
|
||||
let foundCards = [] |
||||
let foundGuilds = [] |
||||
let foundMembers = [] |
||||
let searchResults = [] |
||||
let hashMap = new Map() |
||||
|
||||
let skipcount = 0 |
||||
|
||||
state.serverState.tasks.forEach(t => { |
||||
hashMap.set(t.taskId, t) |
||||
}) |
||||
|
||||
try { |
||||
let regex = new RegExp(search, 'i') |
||||
|
||||
state.serverState.tasks.every(t => { |
||||
const testName = regex.test(t.name) |
||||
|
||||
if (t.guild && (testName || regex.test(t.guild))) { |
||||
if (skipcount < skip) { |
||||
skipcount += 1 |
||||
} else { |
||||
foundGuilds.push(t) |
||||
} |
||||
} else if (regex.test(t.name)) { |
||||
if ( |
||||
!foundGuilds.some(g => { |
||||
return g.guild === t.name |
||||
}) |
||||
) { |
||||
if (skipcount < skip) { |
||||
skipcount += 1 |
||||
} else { |
||||
foundCards.push(t) |
||||
} |
||||
} |
||||
} |
||||
if (foundGuilds.length + foundCards.length >= take) { |
||||
return false |
||||
} else { |
||||
return true |
||||
} |
||||
}) |
||||
|
||||
state.serverState.members.forEach(member => { |
||||
if (regex.test(member.name)) { |
||||
let result = hashMap.get(member.memberId) |
||||
|
||||
// This was introduced as a response to cross-AO cards breaking search
|
||||
if (result) { |
||||
result.name = member.name |
||||
foundMembers.push(result) |
||||
} |
||||
} |
||||
}) |
||||
const searchResults = { |
||||
missions: foundGuilds, |
||||
members: foundMembers, |
||||
tasks: foundCards, |
||||
all: foundGuilds.concat(foundMembers, foundCards), |
||||
length: foundGuilds.length + foundMembers.length + foundCards.length, |
||||
} |
||||
res.status(200).send(searchResults) |
||||
} catch (err) { |
||||
console.log('regex search terminated in error: ', err) |
||||
res.status(500).send('Something went wrong...') |
||||
} |
||||
}) |
||||
app.get('/*', (req, res) => { |
||||
console.log('any route detected') |
||||
res.sendFile(path.join(__dirname, '../../dist/index.html')) |
||||
}) |
||||
} |
@ -0,0 +1,265 @@
|
||||
// import Signal from '@throneless/libsignal-service'
|
||||
import events from './events.js' |
||||
import state from './state.js' |
||||
const serverState = state.serverState |
||||
// import ByteBuffer from 'bytebuffer'
|
||||
import fs from 'fs' |
||||
import path from 'path' |
||||
import { exec } from 'child_process' |
||||
|
||||
export function sendNotification(memberId, message) { |
||||
const member = serverState.members.find( |
||||
member => member.memberId === memberId |
||||
) |
||||
if (!member || !member.phone) { |
||||
console.log( |
||||
"Attempted to notify a member who hasn't entered a phone number!" |
||||
) |
||||
return |
||||
} |
||||
const signalCommand = `signal-cli -u ${process.env.SIGNAL_PHONE} send -m "${message}" ${member.phone}` |
||||
console.log('signalCommand is', signalCommand) |
||||
const dir = exec(signalCommand, function (err, stdout, stderr) { |
||||
if (err) { |
||||
console.log(`Signal send to ${member.name} at ${member.phone} failed`) |
||||
} |
||||
console.log(stdout) |
||||
}) |
||||
|
||||
dir.on('exit', function (code) { |
||||
if (code === 0) { |
||||
console.log(`Signal sent to ${member.name} at ${member.phone}!`) |
||||
} else { |
||||
console.log( |
||||
`Signal send to ${member.name} at ${member.phone} failed with code ${code}` |
||||
) |
||||
} |
||||
}) |
||||
// send(member.phone, message)
|
||||
} |
||||
|
||||
// The rest of this is a complete implementation of @throneless/libsignal-service-javascript
|
||||
// The only problem is that it seems like registration must be completed via the same
|
||||
// program that then sends the message, maybe because registration saves a local private key (?)
|
||||
// and so when I registered with signal-cli, I wasn't able to send messages with this library.this
|
||||
// It gave code 401, "Failed to retrieve new device keys for number" which might also just be
|
||||
// rate limiting, so worth another shot. This library does not implement captcha registration, though.
|
||||
// This library is maybe more elegant than requiring the user to install signal-cli, let's fix it.
|
||||
// function printError(error) {
|
||||
// console.log(error)
|
||||
// }
|
||||
|
||||
// const protocolStore = new Signal.ProtocolStore(
|
||||
// new Storage(process.env.AO_SIGNAL_STORE_PATH || '/home/$USER/.ao/signaldata')
|
||||
// )
|
||||
// protocolStore.load()
|
||||
|
||||
// let accountManager
|
||||
// let messageSender
|
||||
// let messageReceiver
|
||||
|
||||
// // console.log('envs are', process.env.SIGNAL_PHONE, process.env.SIGNAL_PASSWORD)
|
||||
// if (process.env.SIGNAL_PHONE && process.env.SIGNAL_PASSWORD) {
|
||||
// accountManager = new Signal.AccountManager(
|
||||
// process.env.SIGNAL_PHONE,
|
||||
// process.env.SIGNAL_PASSWORD,
|
||||
// protocolStore
|
||||
// )
|
||||
// messageSender = new Signal.MessageSender(protocolStore)
|
||||
// messageSender.connect()
|
||||
// console.log('Signal connected')
|
||||
|
||||
// // messageReceiver = new Signal.MessageReceiver(protocolStore)
|
||||
// // messageReceiver.connect()
|
||||
// }
|
||||
|
||||
// export async function requestSMS(username, password, captcha) {
|
||||
// await accountManager.requestSMSVerification().catch(printError)
|
||||
// }
|
||||
|
||||
// function requestVoice(username, password) {
|
||||
// accountManager
|
||||
// .requestVoiceVerification()
|
||||
// .then(result => {
|
||||
// console.log('Calling for verification.')
|
||||
// })
|
||||
// .catch(printError)
|
||||
// }
|
||||
|
||||
// export async function register(username, password, code) {
|
||||
// await accountManager.registerSingleDevice(code).catch(printError)
|
||||
// }
|
||||
|
||||
// function send(number, text, attachment) {
|
||||
// if (!messageSender) {
|
||||
// console.log('Signal was not initialized')
|
||||
// }
|
||||
// let attachments = []
|
||||
// // messageSender.connect().then(() => {
|
||||
// if (attachment) {
|
||||
// Signal.AttachmentHelper.loadFile(attachment)
|
||||
// .then(file => {
|
||||
// attachments.push(file)
|
||||
// })
|
||||
// .then(() => {
|
||||
// messageSender
|
||||
// .sendMessageToNumber({
|
||||
// number,
|
||||
// body: text,
|
||||
// attachments,
|
||||
// })
|
||||
// .then(result => {
|
||||
// console.log(result)
|
||||
// })
|
||||
// .catch(printError)
|
||||
// })
|
||||
// } else {
|
||||
// messageSender
|
||||
// .sendMessageToNumber({
|
||||
// number,
|
||||
// body: text,
|
||||
// attachments,
|
||||
// })
|
||||
// .then(result => {
|
||||
// console.log(result)
|
||||
// })
|
||||
// .catch(printError)
|
||||
// }
|
||||
// // })
|
||||
// }
|
||||
|
||||
// // Numbers is an array of phone numbers in `+15556667777` format
|
||||
// function sendToGroup(groupId, numbers, text, attachment) {
|
||||
// let attachments = []
|
||||
// // messageSender.connect().then(() => {
|
||||
// if (attachment) {
|
||||
// Signal.AttachmentHelper.loadFile(attachment)
|
||||
// .then(file => {
|
||||
// attachments.push(file)
|
||||
// })
|
||||
// .then(() => {
|
||||
// messageSender
|
||||
// .sendMessageToGroup({
|
||||
// groupId,
|
||||
// recipients: numbers,
|
||||
// body: text,
|
||||
// attachments,
|
||||
// })
|
||||
// .then(result => {
|
||||
// console.log(result)
|
||||
// })
|
||||
// .catch(printError)
|
||||
// })
|
||||
// } else {
|
||||
// messageSender
|
||||
// .sendMessageToGroup({
|
||||
// groupId,
|
||||
// recipients: numbers,
|
||||
// body: text,
|
||||
// })
|
||||
// .then(result => {
|
||||
// console.log(result)
|
||||
// })
|
||||
// .catch(printError)
|
||||
// }
|
||||
// // })
|
||||
// }
|
||||
|
||||
// function expire(number, expire) {
|
||||
// // messageSender.connect().then(() => {
|
||||
// messageSender
|
||||
// .sendExpirationTimerUpdateToNumber(number, parseInt(expire))
|
||||
// .then(result => {
|
||||
// console.log(result)
|
||||
// })
|
||||
// .catch(printError)
|
||||
// // })
|
||||
// }
|
||||
|
||||
// // Numbers is an array of phone numbers in `+15556667777` format
|
||||
// function createGroup(name, numbers) {
|
||||
// // messageSender.connect().then(() => {
|
||||
// groupId = Signal.KeyHelper.generateGroupId()
|
||||
// messageSender
|
||||
// .createGroup(numbers, groupId, name)
|
||||
// .then(result => {
|
||||
// console.log('Created group with ID: ', groupId)
|
||||
// })
|
||||
// .catch(printError)
|
||||
// // })
|
||||
// }
|
||||
|
||||
// // Numbers is an array of phone numbers in `+15556667777` format
|
||||
// function leaveGroup(groupId, numbers) {
|
||||
// // messageSender.connect().then(() => {
|
||||
// messageSender
|
||||
// .leaveGroup(groupId, numbers)
|
||||
// .then(result => {
|
||||
// console.log(result)
|
||||
// console.log('Left group with ID: ', groupId)
|
||||
// })
|
||||
// .catch(printError)
|
||||
// // })
|
||||
// }
|
||||
|
||||
// function receive() {
|
||||
// // messageReceiver.connect().then(() => {
|
||||
// messageReceiver.addEventListener('message', ev => {
|
||||
// console.log('*** EVENT ***:', ev)
|
||||
// ev.data.message.attachments.map(attachment => {
|
||||
// messageReceiver.handleAttachment(attachment).then(attachmentPointer => {
|
||||
// Signal.AttachmentHelper.saveFile(attachmentPointer, './').then(
|
||||
// fileName => {
|
||||
// console.log('Wrote file to: ', fileName)
|
||||
// }
|
||||
// )
|
||||
// })
|
||||
// })
|
||||
// if (ev.data.message.group) {
|
||||
// console.log(ev.data.message.group)
|
||||
// console.log(
|
||||
// `Received message in group ${ev.data.message.group.id}: ${ev.data.message.body}`
|
||||
// )
|
||||
// } else {
|
||||
// console.log('Received message: ', ev.data.message.body)
|
||||
// }
|
||||
// ev.confirm()
|
||||
// })
|
||||
// messageReceiver.addEventListener('configuration', ev => {
|
||||
// console.log('Received configuration sync: ', ev.configuration)
|
||||
// ev.confirm()
|
||||
// })
|
||||
// messageReceiver.addEventListener('group', ev => {
|
||||
// console.log('Received group details: ', ev.groupDetails)
|
||||
// ev.confirm()
|
||||
// })
|
||||
// messageReceiver.addEventListener('contact', ev => {
|
||||
// console.log(
|
||||
// `Received contact for ${ev.contactDetails.number} who has name ${ev.contactDetails.name}`
|
||||
// )
|
||||
// ev.confirm()
|
||||
// })
|
||||
// messageReceiver.addEventListener('verified', ev => {
|
||||
// console.log('Received verification: ', ev.verified)
|
||||
// ev.confirm()
|
||||
// })
|
||||
// messageReceiver.addEventListener('sent', ev => {
|
||||
// console.log(
|
||||
// `Message successfully sent from device ${ev.data.deviceId} to ${ev.data.destination} at timestamp ${ev.data.timestamp}`
|
||||
// )
|
||||
// ev.confirm()
|
||||
// })
|
||||
// messageReceiver.addEventListener('delivery', ev => {
|
||||
// console.log(
|
||||
// `Message successfully delivered to number ${ev.deliveryReceipt.source} and device ${ev.deliveryReceipt.sourceDevice} at timestamp ${ev.deliveryReceipt.timestamp}`
|
||||
// )
|
||||
// ev.confirm()
|
||||
// })
|
||||
// messageReceiver.addEventListener('read', ev => {
|
||||
// console.log(
|
||||
// `Message read on ${ev.read.reader} at timestamp ${ev.read.timestamp}`
|
||||
// )
|
||||
// ev.confirm()
|
||||
// })
|
||||
// // })
|
||||
// }
|
@ -0,0 +1,193 @@
|
||||
import { recover, getAll, insertBackup, insertEvent } from './database.js' |
||||
import M from '../mutations.js' |
||||
import { formatDistanceToNow } from 'date-fns' |
||||
import cron from 'cron' |
||||
import torControl from './torControl.js' |
||||
import cash from '../modules/cash.js' |
||||
import members from '../modules/members.js' |
||||
import tasks from '../modules/tasks.js' |
||||
import resources from '../modules/resources.js' |
||||
import memes from '../modules/memes.js' |
||||
import sessions from '../modules/sessions.js' |
||||
import ao from '../modules/ao.js' |
||||
|
||||
const modules = { cash, members, tasks, resources, memes, sessions, ao } |
||||
|
||||
const backupJob = new cron.CronJob({ |
||||
cronTime: '0 0 0 1 * *', |
||||
onTick: backupState, |
||||
start: true, |
||||
timeZone: 'America/Los_Angeles', |
||||
}) |
||||
|
||||
const serverState = { |
||||
ao: [], |
||||
sessions: [], |
||||
members: [], |
||||
tasks: [], |
||||
resources: [], |
||||
memes: [], |
||||
bookings: [], |
||||
cash: { |
||||
address: '', |
||||
alias: 'dctrl', |
||||
currency: 'CAD', |
||||
spot: 0, |
||||
rent: 0, |
||||
cap: 75, |
||||
pay_index: 0, |
||||
usedTxIds: [], |
||||
outputs: [], |
||||
channels: [], |
||||
info: {}, |
||||
theme: 0, |
||||
}, |
||||
} |
||||
|
||||
const pubState = { |
||||
ao: [], |
||||
sessions: [], |
||||
members: [], |
||||
tasks: [], |
||||
resources: [], |
||||
memes: [], |
||||
cash: { |
||||
address: '', |
||||
alias: '', |
||||
currency: 'CAD', |
||||
spot: 0, |
||||
rent: 0, |
||||
cap: 75, |
||||
pay_index: 0, |
||||
usedTxIds: [], |
||||
outputs: [], |
||||
channels: [], |
||||
info: {}, |
||||
theme: 0, |
||||
}, |
||||
} |
||||
|
||||
function setCurrent(state, b) { |
||||
modules.cash.mutations.setCurrent(state.cash, b) |
||||
modules.tasks.mutations.setCurrent(state.tasks, b) |
||||
modules.sessions.mutations.setCurrent(state.sessions, b) |
||||
modules.ao.mutations.setCurrent(state.ao, b) |
||||
modules.members.mutations.setCurrent(state.members, b) |
||||
modules.resources.mutations.setCurrent(state.resources, b) |
||||
modules.memes.mutations.setCurrent(state.memes, b) |
||||
} |
||||
|
||||
function applyBackup(b) { |
||||
let b1 = Object.assign({}, b) |
||||
setCurrent(serverState, b1) |
||||
b.memes = b.memes && b.memes.length > 0 ? b.memes.map(removeSensitive) : [] |
||||
b.resources = b.resources.map(removeSensitive) |
||||
b.members = b.members.map(removeSensitive) |
||||
b.ao = b.ao.map(removeSensitive) |
||||
b.tasks = b.tasks.map(removeSensitive) |
||||
setCurrent(pubState, b) |
||||
} |
||||
|
||||
function applyEvent(state, ev) { |
||||
M.cashMuts(state.cash, ev) |
||||
M.membersMuts(state.members, ev) |
||||
M.resourcesMuts(state.resources, ev) |
||||
M.memesMuts(state.memes, ev) |
||||
M.sessionsMuts(state.sessions, ev) |
||||
M.tasksMuts(state.tasks, ev) |
||||
M.aoMuts(state.ao, ev) |
||||
} |
||||
|
||||
function initialize(callback) { |
||||
console.log('About to try connecting to tor') |
||||
torControl((err, onion) => { |
||||
recover((err, backup) => { |
||||
let ts = 0 |
||||
if (backup.length > 0) { |
||||
ts = backup[0].timestamp |
||||
console.log( |
||||
'\nFound', |
||||
backup.length, |
||||
'AO snapshot' + |
||||
(backup.length === 1 ? '' : 's') + |
||||
' in the database. Applying' + |
||||
(backup.length > 1 ? ' the most recent' : '') + |
||||
' backup from', |
||||
formatDistanceToNow(ts, { |
||||
addSuffix: true, |
||||
}), |
||||
'...\n' |
||||
) |
||||
applyBackup(backup[0]) |
||||
} |
||||
console.log('Loaded state from backup. Applying events since backup...') |
||||
getAll(ts, (err, all) => { |
||||
if (err) return callback(err) |
||||
all.forEach((ev, i) => { |
||||
applyEvent(serverState, Object.assign({}, ev)) |
||||
applyEvent(pubState, removeSensitive(Object.assign({}, ev))) |
||||
if (i > 0 && i % 10000 === 0) { |
||||
console.log('applied ', i, '/', all.length, ' events...') |
||||
} |
||||
}) |
||||
console.log('applied ', all.length, ' events from the database') |
||||
|
||||
callback(null) |
||||
}) |
||||
console.log('Starting monthly backup cron...') |
||||
backupJob.start() |
||||
//backupState() // Uncomment to back up now
|
||||
}) |
||||
|
||||
// We're applying the onion from torControl directly to the state after backup is applied. Kind of hacky, but it works
|
||||
console.log('onion!', 'http://' + onion) |
||||
serverState.cash.address = onion |
||||
pubState.cash.address = onion |
||||
}) |
||||
} |
||||
|
||||
function backupState() { |
||||
console.log( |
||||
"\nTaking a monthly snapshot of the AO's current loaded state to improve performance..." |
||||
) |
||||
insertBackup(serverState) |
||||
console.log('Snapshot saved.') |
||||
} |
||||
|
||||
function removeSensitive(ev) { |
||||
let secretStuff = [ |
||||
'fob', |
||||
'secret', |
||||
'token', |
||||
'email', |
||||
'payment_hash', |
||||
'inboundSecret', |
||||
'outboundSecret', |
||||
'draft', |
||||
'phone', |
||||
] |
||||
if (ev.type === 'member-field-updated') { |
||||
;['fob', 'secret', 'email', 'phone', 'draft'].forEach(str => { |
||||
if (ev.field === str) { |
||||
secretStuff.push('newfield') |
||||
} |
||||
}) |
||||
} |
||||
const omit = (obj, keys) => { |
||||
return Object.fromEntries( |
||||
Object.entries(obj).filter(([k]) => !keys.includes(k)) |
||||
) |
||||
} |
||||
return omit(ev, secretStuff) |
||||
} |
||||
|
||||
const state = { |
||||
serverState, |
||||
pubState, |
||||
initialize, |
||||
applyEvent, |
||||
removeSensitive, |
||||
setCurrent, |
||||
} |
||||
|
||||
export default state |
@ -0,0 +1,36 @@
|
||||
import cron from 'cron' |
||||
import events from './events.js' |
||||
import state from './state.js' |
||||
const serverState = state.serverState |
||||
|
||||
const todo = new cron.CronJob({ |
||||
cronTime: '0 */5 * * * *', |
||||
onTick: autoUncheck, |
||||
start: false, |
||||
timeZone: 'America/Los_Angeles', |
||||
}) |
||||
|
||||
let rsync |
||||
|
||||
function autoUncheck() { |
||||
console.log('autouncheck') |
||||
const now = Date.now() |
||||
serverState.tasks.forEach(task => { |
||||
if (task.claimInterval > 0 && task.claimed.length > 0) { |
||||
const dur = now - task.lastClaimed |
||||
console.log('\n\nDURATION is ', dur) |
||||
const intervalInMs = task.claimInterval * 60 * 60 * 1000 |
||||
if (dur >= intervalInMs) { |
||||
events.trigger( |
||||
'task-reset', |
||||
{ taskId: task.taskId, timestamp: now }, |
||||
null |
||||
) |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
export default function () { |
||||
todo.start() |
||||
} |
@ -0,0 +1,164 @@
|
||||
import fs from 'fs' |
||||
import net from 'net' |
||||
import { v1 as uuidV1 } from 'uuid' |
||||
import { execSync } from 'child_process' |
||||
let PORT = parseInt(process.env.PORT) || 3000 |
||||
|
||||
console.log( |
||||
'Path to tor auth cookie file:', |
||||
process.env.HOME + '/.tor/control_auth_cookie' |
||||
) |
||||
var cookieBuff = fs.readFileSync(process.env.HOME + '/.tor/control_auth_cookie') |
||||
var cookie = Buffer.from(cookieBuff).toString('hex') |
||||
|
||||
let controlClient |
||||
try { |
||||
controlClient = net.connect({ host: '127.0.0.1', port: 9051 }, () => { |
||||
controlClient.write('AUTHENTICATE ' + cookie + '\r\n') |
||||
}) |
||||
} catch (err) { |
||||
console.log( |
||||
"Failed to connect to local tor instance. Is tor running? Try 'systemctl status tor' and 'sudo systemctl restart tor. Exiting.'" |
||||
) |
||||
throw err |
||||
} |
||||
|
||||
let hiddenServicePortSplit |
||||
let hiddenServiceDirSplit |
||||
let onion |
||||
var i = -1 |
||||
|
||||
let targetDir = process.env.HOME + '/.tor/' + uuidV1() // //"/var/lib/tor/" + 'eda29f80-7f28-11ec-b6f2-636d1c517fa0' //uuidV1()
|
||||
console.log('targetDir is', targetDir) |
||||
const torControl = function (callback) { |
||||
controlClient.on('data', x => { |
||||
i++ |
||||
if (i === 0) { |
||||
controlClient.write('GETCONF HiddenServicePort \r\n') |
||||
} else if (i === 1) { |
||||
hiddenServicePortSplit = splitFromBuffer(x) |
||||
hiddenServicePortSplit = hiddenServicePortSplit.filter(x => |
||||
x.includes('HiddenServicePort') |
||||
) |
||||
controlClient.write('GETCONF HiddenServiceDir \r\n') |
||||
} else if (i === 2) { |
||||
const splitDataStepTwo = splitFromBuffer(x) |
||||
//hiddenServiceDirSplit = hiddenServiceDirSplit.filter(x => x !== 'HiddenServiceDir')
|
||||
if (Array.isArray(splitDataStepTwo)) { |
||||
splitDataStepTwo.forEach(hiddenServiceLine => { |
||||
if (hiddenServiceLine.includes('HiddenServiceDir=')) { |
||||
if (!Array.isArray(hiddenServiceDirSplit)) { |
||||
hiddenServiceDirSplit = [] |
||||
} |
||||
hiddenServiceDirSplit.push(hiddenServiceLine) |
||||
} else if (hiddenServiceLine.includes('HiddenServicePort=')) { |
||||
if (!Array.isArray(hiddenServicePortSplit)) { |
||||
hiddenServicePortSplit = [] |
||||
} |
||||
hiddenServicePortSplit.push(hiddenServiceLine) |
||||
} |
||||
}) |
||||
} |
||||
console.log('hiddenServiceDirSplit is', hiddenServiceDirSplit) |
||||
onion = checkCurrentPortHasConfigAndReturnOnion( |
||||
hiddenServicePortSplit, |
||||
hiddenServiceDirSplit, |
||||
PORT |
||||
) |
||||
if (!onion) { |
||||
let newConf = buildNewConfString( |
||||
hiddenServicePortSplit, |
||||
hiddenServiceDirSplit, |
||||
PORT |
||||
) |
||||
controlClient.write('SETCONF ' + newConf + ' \r\n') |
||||
} else { |
||||
controlClient.write('QUIT \r\n') |
||||
callback(null, onion) |
||||
} |
||||
} else if (i === 3) { |
||||
controlClient.write('GETCONF HiddenServicePort \r\n') |
||||
} else if (i === 4) { |
||||
hiddenServicePortSplit = splitFromBuffer(x) |
||||
controlClient.write('GETCONF HiddenServiceDir \r\n') |
||||
} else if (i === 5) { |
||||
hiddenServiceDirSplit = splitFromBuffer(x) |
||||
onion = checkCurrentPortHasConfigAndReturnOnion( |
||||
hiddenServicePortSplit, |
||||
hiddenServiceDirSplit, |
||||
PORT |
||||
) |
||||
if (!onion) { |
||||
console.log('guess we failed') |
||||
callback('sorry') |
||||
} else { |
||||
controlClient.write('QUIT \r\n') |
||||
callback(null, onion) |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
function splitFromBuffer(x) { |
||||
return Buffer.from(x) |
||||
.toString() |
||||
.split(/\r?\n/) |
||||
.filter(x => x) |
||||
.map(x => x.slice(4)) |
||||
} |
||||
|
||||
function buildNewConfString( |
||||
hiddenServicePortSplit, |
||||
hiddenServiceDirSplit = [], |
||||
port |
||||
) { |
||||
console.log('process uid is', process.getuid()) |
||||
try { |
||||
console.log('targetDir is', targetDir) |
||||
|
||||
console.log('making dir: ', fs.mkdirSync(targetDir, '0700')) |
||||
|
||||
let uid = process.getuid() |
||||
|
||||
let gid = Number.parseInt(execSync('id -g tor').toString(), 10) |
||||
console.log('uid is', uid, 'and gid is', gid) |
||||
//fs.chownSync(targetDir, uid, gid)
|
||||
fs.chmodSync(targetDir, '0700') |
||||
} catch (err) { |
||||
console.log(err) |
||||
} |
||||
|
||||
console.log('check hiddenServicePortSplit is', hiddenServicePortSplit) |
||||
hiddenServicePortSplit = hiddenServicePortSplit.map(noQuotes => { |
||||
return noQuotes.slice(0, 18) + '"' + noQuotes.slice(18) + '"' |
||||
}) |
||||
|
||||
let alternate = [] |
||||
hiddenServiceDirSplit.forEach((x, i) => { |
||||
alternate.push(x) |
||||
alternate.push(hiddenServicePortSplit[i]) |
||||
}) |
||||
let conffy = '' |
||||
conffy += alternate.join(' ') |
||||
conffy += ' HiddenServiceDir=' + targetDir + ' ' |
||||
conffy += ' HiddenServicePort="80 127.0.0.1:' + port + '"' |
||||
return conffy |
||||
} |
||||
|
||||
function checkCurrentPortHasConfigAndReturnOnion( |
||||
hiddenServicePortSplit, |
||||
hiddenServiceDirSplit, |
||||
port = 8003 |
||||
) { |
||||
let onion |
||||
hiddenServicePortSplit.forEach((x, i) => { |
||||
if (x.indexOf(port) > -1) { |
||||
let directory = hiddenServiceDirSplit[i].slice(17) |
||||
onion = fs.readFileSync(directory + '/hostname', { encoding: 'utf8' }) |
||||
} |
||||
}) |
||||
|
||||
return onion || false |
||||
} |
||||
|
||||
export default torControl |
@ -0,0 +1,37 @@
|
||||
import state from './state.js' |
||||
|
||||
export function buildResCallback(res, additionalFields?) { |
||||
return (err, dbResponse) => { |
||||
if(dbResponse.changes) { |
||||
// So as not to leak sensitive data, only add additionalFields if the database query was successful
|
||||
Object.assign(dbResponse, additionalFields) |
||||
} |
||||
if (err) { |
||||
res.status(500).send('db err') |
||||
} else { |
||||
res.status(201).send(dbResponse) |
||||
} |
||||
} |
||||
} |
||||
|
||||
export function memberFromFob(fob) { |
||||
let m |
||||
state.serverState.members |
||||
.filter(m => m.active > 0) |
||||
.forEach(member => { |
||||
if (member.fob == fob) { |
||||
m = member |
||||
} |
||||
}) |
||||
return m |
||||
} |
||||
|
||||
export function getResource(resourceId) { |
||||
let resource |
||||
state.serverState.resources.forEach(r => { |
||||
if (r.resourceId == resourceId) { |
||||
resource = r |
||||
} |
||||
}) |
||||
return resource |
||||
} |
@ -0,0 +1,288 @@
|
||||
import state from './state.js' |
||||
import { isAheadOf, isDecidedlyMorePopularThan, isSenpaiOf } from '../members.js' |
||||
|
||||
export default { |
||||
isAmount(val, errRes) { |
||||
let parsed = parseFloat(val) |
||||
if (parsed !== 0 && !parsed) { |
||||
errRes.push('amount must be a number') |
||||
return false |
||||
} |
||||
if (parsed < 0) { |
||||
errRes.push('amount must be positive') |
||||
return false |
||||
} |
||||
return true |
||||
}, |
||||
isField(val, errRes) { |
||||
let isField = |
||||
val === 'name' || |
||||
val === 'email' || |
||||
val === 'secret' || |
||||
val === 'fob' || |
||||
val === 'draft' || |
||||
val === 'tutorial' || |
||||
val === 'phone' || |
||||
val == 'priorityMode' |
||||
if (!isField) { |
||||
errRes.push('invalid field') |
||||
return false |
||||
} |
||||
return isField |
||||
}, |
||||
isAddress(val, errRes) { |
||||
let result = false |
||||
state.serverState.ao.forEach(ao => { |
||||
if (val === ao.address) { |
||||
result = true |
||||
} |
||||
}) |
||||
if (!result) { |
||||
errRes.push('invalid address') |
||||
} |
||||
return result |
||||
}, |
||||
isMemberId(val, errRes) { |
||||
let result = false |
||||
let result2 = false |
||||
state.serverState.members.forEach(member => { |
||||
if (val === member.memberId) { |
||||
result = true |
||||
} |
||||
}) |
||||
state.serverState.tasks.forEach(task => { |
||||
if (val === task.taskId) { |
||||
result2 = true |
||||
} |
||||
}) |
||||
|
||||
if (!result || !result2) { |
||||
errRes.push('invalid member') |
||||
} |
||||
return result |
||||
}, |
||||
isMemberName(val, errRes) { |
||||
let result = false |
||||
state.serverState.members.forEach(member => { |
||||
if (val === member.name) { |
||||
result = true |
||||
} |
||||
}) |
||||
|
||||
if (!result) { |
||||
errRes.push('invalid member') |
||||
} |
||||
return result |
||||
}, |
||||
isActiveMemberId(val, errRes) { |
||||
let result = false |
||||
state.serverState.members.forEach(member => { |
||||
if (val === member.memberId && member.active >= 0) { |
||||
result = true |
||||
} |
||||
}) |
||||
if (!result) { |
||||
errRes.push('invalid member') |
||||
} |
||||
return result |
||||
}, |
||||
isTaskId(val, errRes) { |
||||
let result = false |
||||
state.serverState.tasks.forEach(task => { |
||||
// console.log("AO: server/validators.js: isTaskId: ", {val, task})
|
||||
if (task.taskId == val) { |
||||
result = true |
||||
} |
||||
}) |
||||
if (!result) { |
||||
errRes.push('invalid task') |
||||
} |
||||
// console.log("AO: server/validators.js: isTaskId: ", {result, val, errRes});
|
||||
return result |
||||
}, |
||||
isTaskId_sane(val, errRes) { |
||||
let result = false |
||||
try { |
||||
let taskName = val.trim().toLowerCase() |
||||
if (taskName === val) result = true |
||||
} catch (erorr) { |
||||
// do nothing
|
||||
} |
||||
if (result === false) { |
||||
errRes.push('invalid task id, must be a lower case, trimmed string') |
||||
} |
||||
// console.log("AO: server/validators.js: isTaskId_sane: ", {result, val, errRes})
|
||||
return result |
||||
}, |
||||
taskIdExists(val, errRes) { |
||||
let result = false |
||||
if (!this.isTaskId_sane(val, errRes)) { |
||||
errRes.push('invalid task id') |
||||
} else { |
||||
let taskId = val |
||||
state.serverState.tasks.some(task => { |
||||
if (task.taskId.trim().toLowerCase() === val) { |
||||
result = true |
||||
return true |
||||
} |
||||
}) |
||||
if (result === false) { |
||||
errRes.push( |
||||
'AO: server/validators.js: taskIdExists: task not found: ' + val |
||||
) |
||||
} |
||||
// console.log('AO: server/validators.js: taskIdExists: ', {result, val, errRes});
|
||||
return result |
||||
} |
||||
}, |
||||
isTaskName(val, errRes) { |
||||
let result = false |
||||
state.serverState.tasks.forEach(task => { |
||||
if ( |
||||
task.name.trim().localeCompare(val.trim(), undefined, { |
||||
sensitivity: 'base', |
||||
}) |
||||
) { |
||||
result = true |
||||
} |
||||
}) |
||||
if (result) { |
||||
errRes.push('invalid task') |
||||
} |
||||
// console.log("AO: server/validators.js: isTaskName: ", {result, val, errRes});
|
||||
return !result |
||||
}, |
||||
isTaskName_sane(val, errRes) { |
||||
let result = false |
||||
try { |
||||
let taskName = val.trim().toLowerCase() |
||||
if (taskName === val) result = true |
||||
} catch (erorr) { |
||||
// do nothing
|
||||
} |
||||
if (result === false) { |
||||
errRes.push('invalid task name, must be a lower case, trimmed string') |
||||
} |
||||
// console.log("AO: server/validators.js: isTaskName_sane: ", {result, val, errRes})
|
||||
return result |
||||
}, |
||||
taskNameExists(val, errRes) { |
||||
let result = false |
||||
if (!this.isTaskName_sane(val, errRes)) { |
||||
errRes.push('invalid task') |
||||
} else { |
||||
let taskName = val |
||||
state.serverState.tasks.some(task => { |
||||
if (task.name.trim().toLowerCase() === val) { |
||||
result = true |
||||
return true |
||||
} |
||||
}) |
||||
if (result === false) { |
||||
errRes.push( |
||||
'AO: server/validators.js: taskNameExists: task not found: ' + val |
||||
) |
||||
} |
||||
// console.log("AO: server/validators.js: taskNameExists: ", {result, val, errRes});
|
||||
return result |
||||
} |
||||
}, |
||||
isSession(val, errRes) { |
||||
let result = false |
||||
state.serverState.sessions.forEach(s => { |
||||
if (val === s.session) { |
||||
result = true |
||||
} |
||||
}) |
||||
if (!result) { |
||||
errRes.push('invalid session') |
||||
} |
||||
return result |
||||
}, |
||||
isResourceId(val, errRes) { |
||||
let result = false |
||||
state.serverState.resources.forEach(resource => { |
||||
if (val === resource.resourceId) { |
||||
result = true |
||||
} |
||||
}) |
||||
if (!result) { |
||||
errRes.push('invalid resource') |
||||
} |
||||
return result |
||||
}, |
||||
isNewResourceId(val, errRes) { |
||||
let isNew = true |
||||
state.serverState.resources.forEach(resource => { |
||||
if (val == resource.resourceId) { |
||||
isNew = false |
||||
} |
||||
}) |
||||
if (!isNew) { |
||||
errRes.push('resourceId exists') |
||||
} |
||||
return isNew |
||||
}, |
||||
isBool(val, errRes) { |
||||
let isBool = typeof val === 'boolean' |
||||
if (!isBool) { |
||||
errRes.push('field requires boolean') |
||||
} |
||||
return isBool |
||||
}, |
||||
isNotes(val, errRes) { |
||||
return true |
||||
}, |
||||
isCoord(val, errRes) { |
||||
let result = true |
||||
const lbx = 0 |
||||
const lby = 0 |
||||
const ubx = 16 |
||||
const uby = 16 |
||||
|
||||
let bx = lbx <= val.x && val.x <= ubx |
||||
let by = lby <= val.y && val.y <= uby |
||||
if (!(by && bx) && Number.isInteger(val.x) && Number.isInteger(val.y)) { |
||||
result = false |
||||
// errRes.push("invalid grid coord");
|
||||
} |
||||
return result |
||||
}, |
||||
isColor(val, errRes) { |
||||
let colors = ['red', 'yellow', 'green', 'purple', 'blue'] |
||||
return colors.indexOf(val) >= 0 |
||||
}, |
||||
isAheadOf(senpaiId, kohaiId, errRes) { |
||||
return isAheadOf(senpaiId, kohaiId, state.serverState, errRes) |
||||
}, |
||||
isDecidedlyMorePopularThan(senpaiId, kohaiId, errRes) { |
||||
return isDecidedlyMorePopularThan( |
||||
senpaiId, |
||||
kohaiId, |
||||
state.serverState, |
||||
errRes |
||||
) |
||||
}, |
||||
isSenpaiOf(senpaiId, kohaiId, errRes) { |
||||
return isSenpaiOf(senpaiId, kohaiId, state.serverState, errRes) |
||||
}, |
||||
hasBanOn(senpaiId, kohaiId, errRes) { |
||||
const kohai = state.serverState.members.find(t => t.memberId === kohaiId) |
||||
if (!kohai) { |
||||
console.log('invalid member detected') |
||||
errRes.push('invalid member detected') |
||||
return false |
||||
} |
||||
if (!kohai.hasOwnProperty('potentials')) { |
||||
console.log('no ban to remove') |
||||
|
||||
errRes.push('no ban exists to remove') |
||||
return false |
||||
} |
||||
console.log('will return true if it finds it now') |
||||
|
||||
return kohai.potentials.some( |
||||
pot => pot.memberId === senpaiId && pot.opinion === 'member-banned' |
||||
) |
||||
}, |
||||
} |
@ -0,0 +1,351 @@
|
||||
// Every card has a color, right now only five preset colors exist (they can be renamed with the glossary and recolored with CSS)
|
||||
export type Color = 'red' | 'yellow' | 'green' | 'purple' | 'blue' | 'black' |
||||
|
||||
// The regions or areas within a card
|
||||
// There are four main zones within a card: the priorities,
|
||||
// the optional pinboard (grid/pyramid/rune),the subTasks (main pile), and completed cards
|
||||
export type CardZone = |
||||
| 'card' // The card itself, the whole card
|
||||
| 'priorities' // The card's priorities section (right card drawer)
|
||||
| 'grid' // A pinboard that can be added to the card (shows on card)
|
||||
| 'subTasks' // The main pile of subcards within this card (bottom card drawer)
|
||||
| 'completed' // Checked-off tasks archived when discarded (viewable in priorities card drawer)
|
||||
| 'context' // The context area above a card that contains the card history (cards can be dragged from here)
|
||||
| 'discard' // The background of the page behind the card, where cards can be dropped to discard
|
||||
| 'panel' // Any other unspecified side panel where cards can be dragged out of (to copy, not move, the card)
|
||||
| 'gifts' // The gifts area at the top left of the member card where cards you receive accumulate.
|
||||
| 'stash' // The stashed cards area of the card (right card drawer)
|
||||
|
||||
// A card's pinboard can be in one of three styles
|
||||
export type PinboardStyle = 'grid' | 'pyramid' | 'rune' |
||||
|
||||
// The global left sidebar can open and display one of these tabs at a time
|
||||
export type LeftSidebarTab = |
||||
| 'hub' |
||||
| 'gifts' |
||||
| 'guilds' |
||||
| 'members' |
||||
| 'calendar' |
||||
| 'bounties' |
||||
| 'manual' |
||||
| 'search' |
||||
| 'deck' |
||||
|
||||
// The global right sidebar can display one of these Bull tabs
|
||||
export type RightSidebarTab = 'resources' | 'p2p' | 'crypto' | 'membership' |
||||
|
||||
// The right side of a card displays these tabs, which can be clicked to open the corresponding card drawer
|
||||
export type CardTab = 'priorities' | 'timecube' | 'lightning' |
||||
|
||||
// When a member gifts/sends/passes a card to another member, the cards .pass array holds an array of passes
|
||||
// The 0th element holds the memberId of the sender, and the 1st element holds the memberId of the recipient
|
||||
export type CardPass = string[2] |
||||
|
||||
// Definition of an AO
|
||||
export interface AoState { |
||||
session: string |
||||
token: string |
||||
loggedIn: boolean |
||||
user: string |
||||
ao: ConnectedAo[] |
||||
sessions: Session[] |
||||
members: Member[] |
||||
tasks: Task[] |
||||
resources: Resource[] |
||||
memes: Meme[] |
||||
socketState?: string |
||||
protectedRouteRedirectPath?: string |
||||
bookings?: Booking[] // Used on server to track calendar events on a timer
|
||||
cash: { |
||||
address: string |
||||
alias: string |
||||
currency: string |
||||
spot: number |
||||
rent: number |
||||
cap: number |
||||
quorum: number |
||||
pay_index: number |
||||
usedTxIds: number[] |
||||
outputs: Output[] |
||||
channels: Channel[] |
||||
info: SatInfo |
||||
} |
||||
loader?: { |
||||
token: string |
||||
session: string |
||||
connected: string |
||||
connectionError: string |
||||
reqStatus: string |
||||
lastPing: number |
||||
} |
||||
} |
||||
|
||||
// An AO serves its members, who each have an account on the AO server.
|
||||
export interface Member { |
||||
type: 'member-created' // The event is added directly to the database so it has this as an artifact, could filter on member-created and remove here
|
||||
name: string // The name of the member
|
||||
memberId: string // The unique UUID of the member
|
||||
address: string // ???
|
||||
active: number // The member's active status. Number increases each consecutive active month.
|
||||
balance: number // Member's point balance
|
||||
badges: [] // Badges that the member has collected
|
||||
tickers: Ticker[] // Customizable list of crypto tickers on the member's right sidebar
|
||||
info: {} // ???
|
||||
timestamp: number // When the member was created
|
||||
lastUsed: number // Last time the member logged in, used a resource, or barked
|
||||
muted: boolean // Whether the member has sound effects turned on or off (sound effects not currently implemented)
|
||||
priorityMode: boolean // Whether the member has activated Priority Mode, which shows the first priority above its parent card
|
||||
fob: string // The member can enter a fob ID number from a physical fob, saved here for when they tap
|
||||
potentials: Signature[] // List of potential actions built up on the member (not currently in use)
|
||||
banned: boolean // True if the member is currently banned (member continues to exist)
|
||||
draft: string // The member's currently-saved draft (also saved on client)
|
||||
tutorial?: boolean // Whether the member has completed the initial interactive tour of the AO
|
||||
p0wned?: boolean // Whether the member has had their password reset (changes to false when they set it themselves)
|
||||
phone?: string // Phone number used for Signal notifications
|
||||
} |
||||
|
||||
// A member can create and collect cards. The words 'card' and 'task' are used as synonyms, because the AO is meant for action.
|
||||
export interface Task { |
||||
taskId: string // ao-react: Random UUID | ao-3: CRC-32 hash of the content
|
||||
name: string // The text of the card, the main content. Can be plain text, Markdown, or HTML and JavaScript (optimized for injection).
|
||||
color: Color // Color of the card as a word | Future: Could be any color word or hex code paired with a word naming the color
|
||||
deck: string[] // *Array of memberIds of members who grabbed and are holding the card in their deck
|
||||
guild: string | boolean // Optional guild / pin / tag title for the card. This is editable (unlike cards currently). Guild cards are indexed in Guilds sidebar on left. (Value of 'true' could mean that guild name equals card text [not implemented yet].)
|
||||
address: string // Mainnet bitcoin address for this card (generated by calling address)
|
||||
bolt11?: string // Lightning network bitcoin address for this carde (generated by calling invoice-created)
|
||||
payment_hash: string //
|
||||
book?: Booking // Book/schedule this card as an event on the calendar
|
||||
priorities: string[] // *Array of taskIds of cards prioritized within this card
|
||||
subTasks: string[] // *Array of taskIds of cards within this card
|
||||
completed: string[] // *Array of taskIds of checked-off completed cards within this cards. Cards saved here when discarded with any checkmarks on them.
|
||||
pinboard?: Pinboard | null // *Pinboard object containing pinboard properties
|
||||
pins?: Pin[] // *New way of doing the Grid, Pyramid, and upcoming Rune layouts for the card
|
||||
parents: string[] // *List of this cards parents, ought to be kept updated by mutations.
|
||||
claimed: string[] // Lists of taskIds who have checked this card (thus claiming the bounty)
|
||||
claimInterval?: number // Automatic uncheck timer in milliseconds [this feature will change to uncheck the cards within]
|
||||
//uncheckInterval // Rename of claimInterval to be rolled out
|
||||
uncheckThisCard?: boolean // Unchecks this card every uncheckInterval if true
|
||||
uncheckPriorities?: boolean // Unchecks prioritized cards every uncheckInterval if true
|
||||
uncheckPinned?: boolean // Unchecks pinned cards every uncheckInterval if true (maybe could combine with uncheckPriorities)
|
||||
dimChecked?: boolean // If true, checked cards on the pinboard and in the priorities list will display visually dimmed to make tasking easier
|
||||
signed: Signature[] // Members can explicitly sign cards to endorse them (future option to counter-sign as -1 is already built-in)
|
||||
passed: string[][] // Array of [senderMemberId, receiverMemberId] pairs of pending gifts sent to the receiving member. Cleared when opened.
|
||||
giftCount?: number // Count of unopened gift cards that ought to be kept automatically updated, for showing this number ot other members
|
||||
lastClaimed: number // The last time someone checked this card off (Unix timestamp)
|
||||
allocations: Allocation[] // List of points temporarily allocated to this card from parent cards, making this card a claimable bounty
|
||||
boost: number // Bonus points on the card (?)
|
||||
goal?: number // Optional points goal shows after the current number of points on a card, e.g., 8/10 points raised in the crowdfund.
|
||||
highlights: number[] |
||||
seen: UserSeen[] // Array of events marking the first (?) or most recent (?) time they looked at the card. Used for unread markers.
|
||||
timelog?: LabourTime[] // Arary of timelog events on the card
|
||||
created: number // When the card was created (Unix timestamp)
|
||||
showChatroom?: boolean // Whether or not to show the chatroom tab. Only allowed on cards with a .guild set for simplicity and transparency's sake.
|
||||
avatars?: AvatarLocation[] // When a member joins a chatroom, it shows they are "at" that card. | Future: Little avator icons that can be moved from card to card or clicked to follow.
|
||||
memberships?: Membership[] // Members can "join" a card as members. The first member is automatically Level 2 and can boss the Level 1's around. You can decrease your level and lose your power.
|
||||
showStash?: boolean // Whether or not to show the stash tab. Only allowed on cards with a .guild set for simplicity and transparency's sake.
|
||||
stash?: { |
||||
// *Stash of more cards associated with this card. Members have access to stashes of their level and below.
|
||||
[key: number]: string[] // Each numbered stash level contains a list of taskIds of cards stored in that stash level.
|
||||
} |
||||
unionHours?: number // Number of estimated hours for the task
|
||||
unionSkill?: number // Skill level required for the task (0-5)
|
||||
unionHazard?: number // Hazard level for the task (0-5)
|
||||
loadedFromServer?: boolean // True if the card has been loaded from the server, false if empty placeholder taskId object
|
||||
stars?: number // Can be simple number or later a Rating[]
|
||||
touches?: number // New feature to count number of time a card was handled, to identify popular cards and personal hotspots.
|
||||
aoGridToolDoNotUpdateUI?: boolean // Rendering hack, maybe this can be improved and removed
|
||||
// *These properties contain taskIds of cards that are within or closely associated with this card, for the purposes of search, content buffering, etc.
|
||||
} |
||||
|
||||
// A booked/scheduled event or resource
|
||||
export interface Booking { |
||||
memberId: string // The member that scheduled the event (?)
|
||||
startTs: number // The start of the event (Unix timestamp)
|
||||
endTs: number // The end time of the event. Optional—but if omitted behavior is undefined. (Unix timestamp)
|
||||
} |
||||
|
||||
// An AO can connect to another AO over tor to send cards
|
||||
export interface ConnectedAo { |
||||
name?: string |
||||
address: string |
||||
outboundSecret: false | string |
||||
inboundSecret: string |
||||
lastContact: number |
||||
links: string[] |
||||
} |
||||
|
||||
// Hardware devices can be connected to the AO as resources over LAN. Resources can be activated in the Bull (right sidebar).
|
||||
export interface Resource { |
||||
resourceId: string // UUID of the resource
|
||||
name: string |
||||
charged: number // How many points it costs to use the resource each time
|
||||
secret: string // ???
|
||||
trackStock: boolean // If true, the resource will track its inventory
|
||||
stock: number // Simple numeric tracking of inventory stock, works for most things
|
||||
} |
||||
|
||||
// Files detected in the ~/.ao/memes folder are each loaded as a Meme
|
||||
export interface Meme { |
||||
memeId: string // UUID that matches the corresponding taskId of the card that is created in lockstep with the Meme object.
|
||||
filename: string // Just the filename and extension, not the path
|
||||
hash: string // Hash of the file
|
||||
filetype: string |
||||
} |
||||
|
||||
// Cordinates of a card on a pinboard or in a list of cards
|
||||
export interface Coords { |
||||
x?: number |
||||
y: number |
||||
} |
||||
|
||||
// Specifies a card taskId at a given location at a specific location within another card.
|
||||
// For pinboard locations, .coords must coordinates for the current pinboard type
|
||||
// For stashed cards, .level specifies which stash level the card is stored in
|
||||
export interface CardLocation { |
||||
taskId?: string // Optional because sometimes a CardLocation is used to described a location where a card will be played/placed
|
||||
inId?: string |
||||
zone?: CardZone // Optional because sometimes a CardLocation describes a card that is not in any location yet
|
||||
level?: number |
||||
coords?: Coords |
||||
} |
||||
|
||||
// An atomic card play, defining a card to remove (from) and a card to place (to)
|
||||
export interface CardPlay { |
||||
from: CardLocation |
||||
to: CardLocation |
||||
} |
||||
|
||||
// Defines the dimensions and other properties of a spread or layout of cards.
|
||||
// Could be expanded or inherited from to create new types of spreads such as a freeform canvas or non-euclidian pinboard.
|
||||
export interface Pinboard { |
||||
spread: PinboardStyle |
||||
height: number |
||||
width?: number |
||||
size: number // Size of squares, roughly in ems
|
||||
} |
||||
|
||||
// A card pinned to a pinboard
|
||||
// Pinboards set to rune layout only use x (y always 0)
|
||||
export interface Pin { |
||||
taskId: string |
||||
y: number |
||||
x?: number |
||||
} |
||||
|
||||
// A guild can temporarily allocate some of its points to a task in its priorities
|
||||
// Anyone who checks off the task will claim the points on it, including all its allocations
|
||||
// Allocated points are actually moved from the parent card at the time (in state) and moved back if the card is deprioritized
|
||||
// All mutations related to moving points around must be perfect to prevent double spend issues
|
||||
export interface Allocation { |
||||
type?: string |
||||
taskId: string |
||||
allocatedId: string |
||||
amount: number |
||||
blame?: string |
||||
} |
||||
|
||||
// A member may sign a card with a positive (1, default), neutral (0), or opposing (-1) opinion, or instead sign with a note/comment
|
||||
// This can be used for votes, upvotes/downvotes (and maybe reaction emojis? reactions could replace signing)
|
||||
export interface Signature { |
||||
memberId: string |
||||
timestamp: Date |
||||
opinion: number | string |
||||
} |
||||
|
||||
// A member may rate a card 0-5 stars
|
||||
export interface Rating { |
||||
memberId: string |
||||
stars: number |
||||
} |
||||
|
||||
// A card is marked when a member sees it for the first time. Used to mark unread cards (feature currently inactive).
|
||||
export interface UserSeen { |
||||
memberId: string |
||||
timestamp: Date |
||||
} |
||||
|
||||
// Log of one duration of time to the timelog on a card
|
||||
export interface LabourTime { |
||||
memberId: string |
||||
taskId: string |
||||
inId: string |
||||
start: number |
||||
stop: number |
||||
} |
||||
|
||||
// Each member has an "avatar" (currently no icon yet) that can be placed on a card to show your location to other members.
|
||||
// When you join a chatroom on a card, you automatically hop your avatar to that card (task-visited event).
|
||||
export interface AvatarLocation { |
||||
memberId: string |
||||
timestamp: number |
||||
area: number |
||||
} |
||||
|
||||
// Members can join guilds. Members of a guild have a numeric level.
|
||||
// Each level has a stash of cards that only members of that level or above can edit.
|
||||
// Members of higher levels can change the level of members with the same or lower levels.
|
||||
// The system is stupid and you can increase your own level too low and lose your power, or too high and mess up the stash.
|
||||
export interface Membership { |
||||
memberId: string |
||||
level: number |
||||
} |
||||
|
||||
// Members can add tickers in the sidebar, which show the exchange rate between a 'from' and 'to' currency (three-letter currency abbreviation)
|
||||
export interface Ticker { |
||||
from: string |
||||
to: string |
||||
} |
||||
|
||||
export interface Output { |
||||
value: number |
||||
} |
||||
|
||||
export interface Channel { |
||||
channel_sat: number |
||||
channel_total_sat: number |
||||
} |
||||
|
||||
// A browser session object
|
||||
export interface Session { |
||||
type: 'session-created' // Event is added directly to the database, this is an artifact of that
|
||||
session: string // Session string?
|
||||
ownerId: string // MemeberId of the session owner
|
||||
timestamp: Date // When the session was created
|
||||
} |
||||
|
||||
export interface LightningChannel { |
||||
peer_id?: any |
||||
funding_txid?: any |
||||
state?: any |
||||
connected?: boolean |
||||
channel_total_sat: number |
||||
channel_sat: number |
||||
} |
||||
|
||||
export interface SatInfo { |
||||
channels?: LightningChannel[] |
||||
mempool?: { sampleTxns: any[]; size: any; bytes: any } |
||||
blockheight?: number |
||||
blockfo?: any |
||||
id?: any |
||||
outputs?: any[] |
||||
address?: { address: string }[] |
||||
} |
||||
|
||||
export interface SearchResults { |
||||
query: string |
||||
page: number |
||||
missions: Task[] |
||||
members: Task[] |
||||
tasks: Task[] |
||||
all: Task[] |
||||
length: number |
||||
} |
||||
|
||||
export const emptySearchResults = { |
||||
missions: [], |
||||
members: [], |
||||
tasks: [], |
||||
all: [], |
||||
length: 0, |
||||
} |
@ -0,0 +1,44 @@
|
||||
export const cancelablePromise = promise => { |
||||
let isCanceled = false |
||||
|
||||
const wrappedPromise = new Promise((resolve, reject) => { |
||||
promise.then( |
||||
value => (isCanceled ? reject({ isCanceled, value }) : resolve(value)), |
||||
error => reject({ isCanceled, error }) |
||||
) |
||||
}) |
||||
|
||||
return { |
||||
promise: wrappedPromise, |
||||
cancel: () => (isCanceled = true), |
||||
} |
||||
} |
||||
export const noop = () => {} |
||||
|
||||
export const delay = n => new Promise(resolve => setTimeout(resolve, n)) |
||||
|
||||
export const isObject = obj => { |
||||
return Object.prototype.toString.call(obj) === '[object Object]' |
||||
} |
||||
|
||||
export const convertToDuration = (milliseconds: number) => { |
||||
const stringifyTime = (time: number): string => String(time).padStart(2, '0') |
||||
const seconds = Math.floor(milliseconds / 1000) |
||||
const minutes = Math.floor(seconds / 60) |
||||
const hours = Math.floor(minutes / 60) |
||||
return `${stringifyTime(hours)}:${stringifyTime( |
||||
minutes % 60 |
||||
)}:${stringifyTime(seconds % 60)}` |
||||
} |
||||
|
||||
export const convertToTimeWorked = (milliseconds: number) => { |
||||
const seconds = Math.floor(milliseconds / 1000) |
||||
const minutes = Math.floor(seconds / 60) |
||||
const hours = Math.floor(minutes / 60) |
||||
|
||||
if (hours > 0) { |
||||
return `${hours}h, ${minutes % 60}m` |
||||
} else { |
||||
return `${minutes % 60}m` |
||||
} |
||||
} |
@ -0,0 +1,33 @@
|
||||
{ |
||||
"compilerOptions": { |
||||
"baseUrl": "./dist/src", |
||||
"rootDir": "src", |
||||
"outDir": "dist", |
||||
"paths": { |
||||
"@config/*": ["./*"], |
||||
"*": [ |
||||
"node_modules/*", |
||||
] |
||||
}, |
||||
"lib": ["ESNext"], |
||||
"target": "ES2020", |
||||
"module": "NodeNext", |
||||
"moduleResolution": "NodeNext", |
||||
"esModuleInterop": true, |
||||
"types": ["node", "vite/client"], |
||||
"allowSyntheticDefaultImports": true, |
||||
"composite": true, |
||||
"incremental": true, |
||||
"sourceMap": true, |
||||
"experimentalDecorators": false, |
||||
"downlevelIteration": true, |
||||
"forceConsistentCasingInFileNames": true, |
||||
"removeComments": true, |
||||
"plugins": [ { "transform": "typescript-transform-paths" } ], |
||||
"allowSyntheticDefaultImports": true, |
||||
"declaration": true, |
||||
"importHelpers": false, |
||||
"skipLibCheck": true, |
||||
}, |
||||
"include": ["src/server/*", "src/*.ts", "src/modules/*"] |
||||
} |
Loading…
Reference in new issue