├── .gitignore ├── LICENSE ├── README.md ├── blobs └── blobs.go ├── channels └── channels.go ├── cmd ├── gopub │ └── main.go ├── sbot │ ├── .gitignore │ ├── main.go │ ├── rpc │ │ ├── feed.go │ │ └── gossip.go │ ├── static │ │ ├── css │ │ │ └── bootstrap.min.css │ │ └── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ ├── templates │ │ ├── content │ │ │ ├── about.tpl │ │ │ ├── avatar.tpl │ │ │ ├── channel.tpl │ │ │ ├── contact.tpl │ │ │ ├── follow.tpl │ │ │ ├── git-repo.tpl │ │ │ ├── git-update.tpl │ │ │ ├── issue.tpl │ │ │ ├── message.tpl │ │ │ ├── post.tpl │ │ │ ├── vote-simple.tpl │ │ │ └── vote.tpl │ │ └── pages │ │ │ ├── addpub.tpl │ │ │ ├── admin.tpl │ │ │ ├── blob.tpl │ │ │ ├── channel.tpl │ │ │ ├── feed.tpl │ │ │ ├── header.tpl │ │ │ ├── index.tpl │ │ │ ├── message.tpl │ │ │ ├── navbar.tpl │ │ │ ├── post.tpl │ │ │ ├── profile.tpl │ │ │ ├── repo.tpl │ │ │ ├── search.tpl │ │ │ ├── thread.tpl │ │ │ └── upload.tpl │ └── webui.go └── sbotcli │ ├── .gitignore │ └── main.go ├── compression.go ├── dns └── dns.go ├── doc.go ├── encoding.go ├── feed_test.go ├── feeds.go ├── follower.go ├── git └── git.go ├── go-ssb.go ├── gossip └── gossip.go ├── graph └── graph.go ├── keys.go ├── message.go ├── messagetopic.go ├── messagetypes.go ├── muxrpcManager └── muxrpcManager.go ├── ref.go ├── rpc └── rpc.go ├── search └── search.go └── social └── social.go /.gitignore: -------------------------------------------------------------------------------- 1 | feeds.db 2 | *.exe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go-ssb 2 | 3 | Go-ssb is an implementation of [secure-scuttlebutt](https://github.com/ssbc/secure-scuttlebutt) in Golang. 4 | 5 | ### Getting Started 6 | 7 | ```bash 8 | # Clone the repo and dependencies into your gopath 9 | go get github.com/andyleap/go-ssb/... 10 | 11 | # Build the sbot executable 12 | cd go/src/github.com/andyleap/go-ssb/cmd/sbot 13 | go build 14 | 15 | # Run the executable 16 | ./sbot 17 | 18 | # Point a web browser at localhost:9823 to use the client 19 | # Go to localhost:9823/admin to add a pub invite and connect to the network 20 | ``` 21 | 22 | **Join us in #scuttlebutt-go on freenode.** 23 | -------------------------------------------------------------------------------- /blobs/blobs.go: -------------------------------------------------------------------------------- 1 | package blobs 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "encoding/json" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "sync" 13 | 14 | "github.com/andyleap/go-ssb" 15 | "github.com/andyleap/go-ssb/muxrpcManager" 16 | "github.com/andyleap/muxrpc" 17 | "github.com/andyleap/muxrpc/codec" 18 | ) 19 | 20 | type BlobLink struct { 21 | blobLink 22 | } 23 | 24 | type blobLink struct { 25 | Link ssb.Ref `json:"link"` 26 | Size int `json:"size,omitempty"` 27 | } 28 | 29 | func (bl *BlobLink) UnmarshalJSON(b []byte) error { 30 | err := json.Unmarshal(b, &bl.blobLink) 31 | if err != nil { 32 | err = json.Unmarshal(b, &bl.Link) 33 | if err != nil { 34 | return err 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func New(root string, ds *ssb.DataStore) *BlobStore { 41 | return &BlobStore{ 42 | ds: ds, 43 | Root: root, 44 | wait: sync.NewCond(&sync.Mutex{}), 45 | want: map[ssb.Ref]*want{}, 46 | } 47 | } 48 | 49 | func Get(ds *ssb.DataStore) *BlobStore { 50 | return ds.ExtraData("blobStore").(*BlobStore) 51 | } 52 | 53 | type want struct { 54 | amount int 55 | resp chan *muxrpc.Conn 56 | done chan struct{} 57 | } 58 | 59 | type BlobStore struct { 60 | ds *ssb.DataStore 61 | Root string 62 | wait *sync.Cond 63 | 64 | want map[ssb.Ref]*want 65 | wantLock sync.Mutex 66 | } 67 | 68 | func (bs *BlobStore) Add(data []byte) ssb.Ref { 69 | hash := sha256.Sum256(data) 70 | 71 | hexhash := hex.EncodeToString(hash[:]) 72 | pre, hexhash := hexhash[:2], hexhash[2:] 73 | os.MkdirAll(filepath.Join(bs.Root, pre), 0777) 74 | ioutil.WriteFile(filepath.Join(bs.Root, pre, hexhash+".tmp"), data, 0777) 75 | os.Rename(filepath.Join(bs.Root, pre, hexhash+".tmp"), filepath.Join(bs.Root, pre, hexhash)) 76 | 77 | bs.wait.Broadcast() 78 | r, _ := ssb.NewRef(ssb.RefBlob, hash[:], ssb.RefAlgoSha256) 79 | return r 80 | } 81 | 82 | func (bs *BlobStore) Has(r ssb.Ref) bool { 83 | hexhash := hex.EncodeToString(r.Raw()) 84 | 85 | if len(hexhash) < 2 { 86 | return false 87 | } 88 | pre, hexhash := hexhash[:2], hexhash[2:] 89 | if _, err := os.Stat(filepath.Join(bs.Root, pre, hexhash)); !os.IsNotExist(err) { 90 | return true 91 | } 92 | return false 93 | } 94 | 95 | func (bs *BlobStore) Size(r ssb.Ref) int64 { 96 | hexhash := hex.EncodeToString(r.Raw()) 97 | 98 | if len(hexhash) < 2 { 99 | return -1 100 | } 101 | pre, hexhash := hexhash[:2], hexhash[2:] 102 | if s, err := os.Stat(filepath.Join(bs.Root, pre, hexhash)); !os.IsNotExist(err) { 103 | return s.Size() 104 | } 105 | return -1 106 | } 107 | 108 | func (bs *BlobStore) WaitFor(r ssb.Ref) { 109 | if bs.Has(r) { 110 | return 111 | } 112 | bs.wait.L.Lock() 113 | defer bs.wait.L.Unlock() 114 | for !bs.Has(r) { 115 | bs.wait.Wait() 116 | } 117 | } 118 | 119 | func (bs *BlobStore) Get(r ssb.Ref) io.ReadCloser { 120 | if !bs.Has(r) { 121 | return nil 122 | } 123 | hexhash := hex.EncodeToString(r.Raw()) 124 | pre, hexhash := hexhash[:2], hexhash[2:] 125 | f, _ := os.Open(filepath.Join(bs.Root, pre, hexhash)) 126 | return f 127 | } 128 | 129 | func (bs *BlobStore) Want(r ssb.Ref) { 130 | bs.wantLock.Lock() 131 | defer bs.wantLock.Unlock() 132 | if bs.Has(r) { 133 | return 134 | } 135 | if _, ok := bs.want[r]; ok { 136 | return 137 | } 138 | w := &want{ 139 | resp: make(chan *muxrpc.Conn), 140 | done: make(chan struct{}), 141 | } 142 | bs.want[r] = w 143 | go func() { 144 | for c := range w.resp { 145 | data := []byte{} 146 | err := c.Source("blobs.get", func(p *codec.Packet) { 147 | data = append(data, p.Body...) 148 | }, r) 149 | if err != nil { 150 | newR := bs.Add(data) 151 | if newR == r { 152 | break 153 | } 154 | } 155 | } 156 | close(w.done) 157 | bs.wantLock.Lock() 158 | defer bs.wantLock.Unlock() 159 | delete(bs.want, r) 160 | }() 161 | conns, ok := bs.ds.ExtraData("muxrpcConns").(*muxrpcManager.ExtraData) 162 | if ok { 163 | conns.Lock.Lock() 164 | defer conns.Lock.Unlock() 165 | for _, conn := range conns.Conns { 166 | go func(conn *muxrpc.Conn) { 167 | has := false 168 | err := conn.Call("blobs.has", &has, r) 169 | if err != nil { 170 | return 171 | } 172 | if has { 173 | w.resp <- conn 174 | } 175 | }(conn) 176 | } 177 | } 178 | } 179 | 180 | func init() { 181 | ssb.RegisterInit(func(ds *ssb.DataStore) { 182 | bs := New("blobs", ds) 183 | ds.SetExtraData("blobStore", bs) 184 | 185 | handlers, ok := ds.ExtraData("muxrpcHandlers").(map[string]func(conn *muxrpc.Conn, req int32, args json.RawMessage)) 186 | if !ok { 187 | handlers = map[string]func(conn *muxrpc.Conn, req int32, args json.RawMessage){} 188 | ds.SetExtraData("muxrpcHandlers", handlers) 189 | } 190 | handlers["blobs.has"] = func(conn *muxrpc.Conn, req int32, rm json.RawMessage) { 191 | var r ssb.Ref 192 | args := []interface{}{&r} 193 | json.Unmarshal(rm, &args) 194 | buf, _ := json.Marshal(bs.Has(r)) 195 | conn.Send(&codec.Packet{ 196 | Req: -req, 197 | Type: codec.JSON, 198 | Body: buf, 199 | }) 200 | } 201 | handlers["blobs.get"] = func(conn *muxrpc.Conn, req int32, rm json.RawMessage) { 202 | var arg1 json.RawMessage 203 | args := []interface{}{&arg1} 204 | json.Unmarshal(rm, &args) 205 | var r ssb.Ref 206 | err := json.Unmarshal(arg1, &r) 207 | if err != nil { 208 | var robj struct { 209 | Hash ssb.Ref `json:"hash"` 210 | Key ssb.Ref `json:"key"` 211 | } 212 | json.Unmarshal(arg1, &robj) 213 | if robj.Key.Type != ssb.RefInvalid { 214 | r = robj.Key 215 | } else if robj.Hash.Type != ssb.RefInvalid { 216 | r = robj.Hash 217 | } 218 | } 219 | 220 | log.Println("Peer asking for ", r.String()) 221 | if !bs.Has(r) { 222 | conn.Send(&codec.Packet{ 223 | Req: -req, 224 | Type: codec.String, 225 | Stream: true, 226 | EndErr: true, 227 | Body: []byte("Blob does not exist"), 228 | }) 229 | return 230 | } 231 | rc := bs.Get(r) 232 | defer rc.Close() 233 | buf := make([]byte, 1024) 234 | log.Println("Sending", r.String()) 235 | for { 236 | n, err := rc.Read(buf[:cap(buf)]) 237 | buf = buf[:n] 238 | if n == 0 { 239 | if err == nil { 240 | continue 241 | } 242 | if err == io.EOF { 243 | break 244 | } 245 | log.Fatal(err) 246 | } 247 | 248 | conn.Send(&codec.Packet{ 249 | Req: -req, 250 | Type: codec.Buffer, 251 | Stream: true, 252 | Body: buf, 253 | }) 254 | if err != nil && err != io.EOF { 255 | log.Fatal(err) 256 | } 257 | } 258 | log.Println("Sent", r.String()) 259 | conn.Send(&codec.Packet{ 260 | Req: -req, 261 | Stream: true, 262 | Type: codec.JSON, 263 | Body: []byte("true"), 264 | EndErr: true, 265 | }) 266 | } 267 | handlers["blobs.changes"] = func(conn *muxrpc.Conn, req int32, rm json.RawMessage) { 268 | } 269 | 270 | peerWants := map[*muxrpc.Conn]chan struct { 271 | id ssb.Ref 272 | val int 273 | }{} 274 | var peerWantsLock sync.Mutex 275 | 276 | handlers["blobs.createWants"] = func(conn *muxrpc.Conn, req int32, rm json.RawMessage) { 277 | peerWantsLock.Lock() 278 | c := peerWants[conn] 279 | peerWantsLock.Unlock() 280 | 281 | for ref := range c { 282 | msg := map[string]int{} 283 | msg[ref.id.String()] = ref.val 284 | buf, _ := ssb.Encode(msg) 285 | log.Println("Telling peer I have ", string(buf)) 286 | conn.Send(&codec.Packet{ 287 | Req: -req, 288 | Type: codec.JSON, 289 | Stream: true, 290 | Body: buf, 291 | }) 292 | } 293 | } 294 | onConnects, ok := ds.ExtraData("muxrpcOnConnect").(map[string]func(conn *muxrpc.Conn)) 295 | if !ok { 296 | onConnects = map[string]func(conn *muxrpc.Conn){} 297 | ds.SetExtraData("muxrpcOnConnect", onConnects) 298 | } 299 | onConnects["blob"] = func(conn *muxrpc.Conn) { 300 | bs.wantLock.Lock() 301 | defer bs.wantLock.Unlock() 302 | peerWantsLock.Lock() 303 | c := make(chan struct { 304 | id ssb.Ref 305 | val int 306 | }, 10) 307 | peerWants[conn] = c 308 | peerWantsLock.Unlock() 309 | 310 | go func() { 311 | conn.Source("blobs.createWants", func(p *codec.Packet) { 312 | var want map[ssb.Ref]int 313 | json.Unmarshal(p.Body, &want) 314 | for id, hopsize := range want { 315 | if bs.Has(id) && hopsize < 0 { 316 | log.Println("I do have ", id, "asked at", hopsize) 317 | c <- struct { 318 | id ssb.Ref 319 | val int 320 | }{id, int(bs.Size(id))} 321 | } 322 | } 323 | }) 324 | peerWantsLock.Lock() 325 | delete(peerWants, conn) 326 | peerWantsLock.Unlock() 327 | }() 328 | 329 | for r, w := range bs.want { 330 | go func(r ssb.Ref, w *want) { 331 | has := false 332 | err := conn.Call("blobs.has", &has, r) 333 | if err != nil { 334 | return 335 | } 336 | if has { 337 | w.resp <- conn 338 | } 339 | }(r, w) 340 | } 341 | } 342 | 343 | }) 344 | } 345 | -------------------------------------------------------------------------------- /channels/channels.go: -------------------------------------------------------------------------------- 1 | package channels 2 | 3 | import ( 4 | "encoding/binary" 5 | "time" 6 | 7 | "github.com/andyleap/go-ssb" 8 | "github.com/andyleap/go-ssb/social" 9 | 10 | "github.com/boltdb/bolt" 11 | ) 12 | 13 | func itob(v int) []byte { 14 | b := make([]byte, 8) 15 | binary.BigEndian.PutUint64(b, uint64(v)) 16 | return b 17 | } 18 | 19 | type Channel struct { 20 | ssb.MessageBody 21 | Channel string `json:"channel"` 22 | Subscribed bool `json:"subscribed"` 23 | } 24 | 25 | func init() { 26 | ssb.MessageTypes["channel"] = func(mb ssb.MessageBody) interface{} { return &Channel{MessageBody: mb} } 27 | ssb.RebuildClearHooks["channels"] = func(tx *bolt.Tx) error { 28 | tx.DeleteBucket([]byte("channels")) 29 | return nil 30 | } 31 | ssb.AddMessageHooks["channels"] = func(m *ssb.SignedMessage, tx *bolt.Tx) error { 32 | _, mb := m.DecodeMessage() 33 | if mbr, ok := mb.(*social.Post); ok { 34 | if mbr.Channel != "" { 35 | channelsBucket, err := tx.CreateBucketIfNotExists([]byte("channels")) 36 | if err != nil { 37 | return err 38 | } 39 | channelBucket, err := channelsBucket.CreateBucketIfNotExists([]byte(mbr.Channel)) 40 | if err != nil { 41 | return err 42 | } 43 | logBucket, err := channelBucket.CreateBucketIfNotExists([]byte("log")) 44 | if err != nil { 45 | return err 46 | } 47 | logBucket.FillPercent = 1 48 | seq, err := logBucket.NextSequence() 49 | if err != nil { 50 | return err 51 | } 52 | logBucket.Put(itob(int(seq)), m.Key().DBKey()) 53 | 54 | timeBucket, err := channelBucket.CreateBucketIfNotExists([]byte("time")) 55 | if err != nil { 56 | return err 57 | } 58 | i := int(m.Timestamp * float64(time.Millisecond)) 59 | for timeBucket.Get(itob(i)) != nil { 60 | i++ 61 | } 62 | timeBucket.Put(itob(i), m.Key().DBKey()) 63 | } 64 | } 65 | return nil 66 | } 67 | } 68 | 69 | func GetChannelLatest(ds *ssb.DataStore, channel string, num int, start int) (msgs []*ssb.SignedMessage) { 70 | ds.DB().View(func(tx *bolt.Tx) error { 71 | channelsBucket := tx.Bucket([]byte("channels")) 72 | if channelsBucket == nil { 73 | return nil 74 | } 75 | channelBucket := channelsBucket.Bucket([]byte(channel)) 76 | if channelBucket == nil { 77 | return nil 78 | } 79 | timeBucket := channelBucket.Bucket([]byte("time")) 80 | if timeBucket == nil { 81 | return nil 82 | } 83 | cursor := timeBucket.Cursor() 84 | _, v := cursor.Last() 85 | for i := 0; i < start; i++ { 86 | _, v = cursor.Prev() 87 | if v == nil { 88 | break 89 | } 90 | } 91 | for l1 := 0; l1 < num; l1++ { 92 | if v == nil { 93 | break 94 | } 95 | m := ds.Get(tx, ssb.DBRef(v)) 96 | msgs = append(msgs, m) 97 | _, v = cursor.Prev() 98 | } 99 | return nil 100 | }) 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /cmd/gopub/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "cryptoscope.co/go/secretstream/secrethandshake" 7 | "github.com/andyleap/go-ssb" 8 | "github.com/andyleap/go-ssb/gossip" 9 | ) 10 | 11 | func main() { 12 | 13 | keypair, err := secrethandshake.LoadSSBKeyPair("secret.json") 14 | if err != nil { 15 | log.Println(err) 16 | } 17 | 18 | datastore, _ := ssb.OpenDataStore("feeds.db", keypair) 19 | 20 | gossip.Replicate(datastore) 21 | 22 | select {} 23 | } 24 | -------------------------------------------------------------------------------- /cmd/sbot/.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | /secret.json 3 | /feeds.db.lock 4 | blobs 5 | sbot 6 | !sbot/ 7 | -------------------------------------------------------------------------------- /cmd/sbot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | // "os" 10 | 11 | "github.com/andyleap/go-ssb" 12 | _ "github.com/andyleap/go-ssb/channels" 13 | "github.com/andyleap/go-ssb/cmd/sbot/rpc" 14 | _ "github.com/andyleap/go-ssb/git" 15 | "github.com/andyleap/go-ssb/gossip" 16 | "github.com/andyleap/go-ssb/graph" 17 | "github.com/andyleap/go-ssb/social" 18 | 19 | "cryptoscope.co/go/secretstream/secrethandshake" 20 | 21 | r "net/rpc" 22 | ) 23 | 24 | var datastore *ssb.DataStore 25 | 26 | func main() { 27 | 28 | keypair, err := secrethandshake.LoadSSBKeyPair("secret.json") 29 | if err != nil { 30 | keypair, err = secrethandshake.GenEdKeyPair(rand.Reader) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | ref, _ := ssb.NewRef(ssb.RefFeed, keypair.Public[:], ssb.RefAlgoEd25519) 36 | sbotKey := struct { 37 | Curve string `json:"curve"` 38 | ID string `json:"id"` 39 | Private string `json:"private"` 40 | Public string `json:"public"` 41 | }{ 42 | Curve: "ed25519", 43 | ID: ref.String(), 44 | Private: base64.StdEncoding.EncodeToString(keypair.Secret[:]) + ".ed25519", 45 | Public: base64.StdEncoding.EncodeToString(keypair.Public[:]) + ".ed25519", 46 | } 47 | buf, _ := ssb.Encode(sbotKey) 48 | err := ioutil.WriteFile("secret.json", buf, 0600) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | } 53 | 54 | datastore, err = ssb.OpenDataStore("feeds.db", keypair) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | defer datastore.Close() 59 | 60 | gossip.Replicate(datastore) 61 | 62 | RegisterWebui() 63 | 64 | //datastore.Rebuild("channels") 65 | 66 | err = r.Register(&Gossip{datastore}) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | err = r.Register(&Feed{datastore}) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | l, _ := net.Listen("tcp", "localhost:9822") 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | 80 | r.Accept(l) 81 | 82 | select {} 83 | } 84 | 85 | type Gossip struct { 86 | ds *ssb.DataStore 87 | } 88 | 89 | func (g *Gossip) AddPub(req rpc.AddPubReq, res *rpc.AddPubRes) error { 90 | gossip.AddPub(g.ds, gossip.Pub{ 91 | Host: req.Host, 92 | Port: req.Port, 93 | Link: ssb.ParseRef(req.PubKey), 94 | }) 95 | return nil 96 | } 97 | 98 | type Feed struct { 99 | ds *ssb.DataStore 100 | } 101 | 102 | func (f *Feed) Post(req rpc.PostReq, res *rpc.PostRes) error { 103 | if req.Feed == "" { 104 | req.Feed = f.ds.PrimaryRef.String() 105 | } 106 | feed := f.ds.GetFeed(ssb.ParseRef(req.Feed)) 107 | 108 | post := &social.Post{} 109 | 110 | post.Text = req.Text 111 | post.Channel = req.Channel 112 | post.Branch = ssb.ParseRef(req.Branch) 113 | post.Root = ssb.ParseRef(req.Root) 114 | post.Type = "post" 115 | 116 | err := feed.PublishMessage(post) 117 | if err != nil { 118 | log.Println(err) 119 | } else { 120 | log.Println("Message ", post, " posted to feed ", feed.ID) 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func (f *Feed) Follow(req rpc.FollowReq, res *rpc.FollowRes) error { 127 | if req.Feed == "" { 128 | req.Feed = f.ds.PrimaryRef.String() 129 | } 130 | feed := f.ds.GetFeed(ssb.ParseRef(req.Feed)) 131 | 132 | follow := &graph.Contact{} 133 | 134 | following := true 135 | follow.Following = &following 136 | follow.Contact = ssb.ParseRef(req.Contact) 137 | follow.Type = "contact" 138 | 139 | err := feed.PublishMessage(follow) 140 | if err != nil { 141 | log.Println(err) 142 | } else { 143 | log.Println("Message ", follow, " posted to feed ", feed.ID) 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func (f *Feed) About(req rpc.AboutReq, res *rpc.AboutRes) error { 150 | if req.Feed == "" { 151 | req.Feed = f.ds.PrimaryRef.String() 152 | } 153 | feed := f.ds.GetFeed(ssb.ParseRef(req.Feed)) 154 | 155 | about := &social.About{} 156 | 157 | about.Name = req.Name 158 | about.About = feed.ID 159 | about.Type = "about" 160 | 161 | err := feed.PublishMessage(about) 162 | if err != nil { 163 | log.Println(err) 164 | } else { 165 | log.Println("Message ", about, " posted to feed ", feed.ID) 166 | } 167 | 168 | return nil 169 | } 170 | -------------------------------------------------------------------------------- /cmd/sbot/rpc/feed.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | type PostReq struct { 4 | Feed string 5 | Text string 6 | Root string 7 | Branch string 8 | Channel string 9 | } 10 | 11 | type PostRes struct { 12 | Err error 13 | } 14 | 15 | type FollowReq struct { 16 | Feed string 17 | Contact string 18 | } 19 | 20 | type FollowRes struct { 21 | Err error 22 | } 23 | 24 | type AboutReq struct { 25 | Feed string 26 | Name string 27 | } 28 | 29 | type AboutRes struct { 30 | Err error 31 | } 32 | -------------------------------------------------------------------------------- /cmd/sbot/rpc/gossip.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | type AddPubReq struct { 4 | Host string 5 | Port int 6 | PubKey string 7 | } 8 | 9 | type AddPubRes struct { 10 | Err error 11 | } 12 | -------------------------------------------------------------------------------- /cmd/sbot/static/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | overflow:auto; 4 | } 5 | body { 6 | margin: 0 auto; 7 | font-family: helvetica, sans-serif; 8 | font-size: 12px; 9 | color: #222; 10 | --main-bg: #eee; 11 | background-color: var(--main-bg); 12 | } 13 | button { 14 | border:none; 15 | background:none; 16 | font-size: inherit; 17 | color: inherit; 18 | } 19 | button:hover { 20 | } 21 | form { 22 | display:inline-block; 23 | } 24 | a { 25 | color: #444499; 26 | text-decoration:none; 27 | } 28 | .avatar{ 29 | width:100px; 30 | height:auto; 31 | max-width: 100px; 32 | max-height: 100px; 33 | border: 0px solid white; 34 | border-radius:10px; 35 | box-shadow: 6px 6px 8px rgba(0,0,0,0.3); 36 | margin:16px; 37 | } 38 | img { 39 | max-width: 70%; 40 | padding:0px; 41 | border: solid 0px white; 42 | margin-left: 6%; 43 | margin-bottom: 10px; 44 | box-shadow: 2px 2px 8px rgba(0,0,0,0.3); 45 | } 46 | .nav-bar { 47 | background-color: #235dc7;; 48 | text-align:center; 49 | padding:0px; 50 | } 51 | .nav-bar a { 52 | color: #eee; 53 | margin: 0px; 54 | text-decoration: none; 55 | padding-top: 10px; 56 | padding-bottom: 10px; 57 | padding-left: 10px; 58 | padding-right: 10px; 59 | } 60 | .nav-bar a:hover { 61 | background-color:#134d97; 62 | color: #fff; 63 | } 64 | .search { 65 | color: #000; 66 | background-color: #134db7; 67 | padding: 1px; 68 | border: 0px solid black; 69 | font-size: inherit; 70 | width: 24ex; 71 | padding-left:10px; 72 | } 73 | .search-input:focus, 74 | .search-input:hover { 75 | background-color:#fff; 76 | } 77 | textarea { 78 | font: inherit; 79 | width: 100%; 80 | } 81 | blockquote { 82 | color:#444; 83 | font-size:80%; 84 | margin-top:12px; 85 | background-color:#efefef; 86 | border-left: solid 4px #ddd; 87 | padding:8px; 88 | } 89 | .coolpost { 90 | border: 1px solid lightgrey; 91 | margin:20px; 92 | padding-left:10px; 93 | padding-right:10px; 94 | background:white; 95 | color:#222; 96 | } 97 | .postblock { 98 | border: 0px solid; 99 | display:inline-block; 100 | margin:2; 101 | padding-bottom:0; 102 | padding-top:0; 103 | width:90%; 104 | } 105 | .votes { 106 | text-align:right; 107 | } 108 | .voteman:hover > .hidebox { 109 | display:block; 110 | } 111 | .hidebox { 112 | display:none; 113 | right:100; 114 | position:absolute; 115 | margin:auto; 116 | } 117 | .goofyBS .avatar { 118 | width:60px; 119 | height:60px; 120 | padding:0px; 121 | margin-right:0px; 122 | } 123 | .goofyBS { 124 | width:100%; 125 | } 126 | .facebox { 127 | color:#999; 128 | float:left; 129 | font-size:80%; 130 | padding-left:14px; 131 | } 132 | .postingarea { 133 | width:600px; 134 | height:120px; 135 | margin:auto; 136 | margin-top:30; 137 | } 138 | .vote { 139 | display:inline-block; 140 | text-align:center; 141 | width:120; 142 | } 143 | .votebox { 144 | text-align:center; 145 | } 146 | .profilebox { 147 | color:#666; 148 | } 149 | .avatarwell { 150 | background:white; 151 | height:300; 152 | width:300; 153 | margin-top:30; 154 | display:inline-block; 155 | float:left; 156 | padding:9; 157 | } 158 | .about { 159 | float:left; 160 | display:inline-block; 161 | font-size:120%; 162 | padding:100; 163 | text-align:center; 164 | color:#444494; 165 | } 166 | .ref { 167 | font-size:70%; 168 | color:#000; 169 | } 170 | .friendstray { 171 | background:#ddd; 172 | margin:auto; 173 | margin-bottom:20; 174 | margin-top:20; 175 | padding:20; 176 | width:70%; 177 | border-radius:30; 178 | } 179 | .invitebutton { 180 | background-color: #235dc7;; 181 | text-align:center; 182 | font-size:200%; 183 | padding:0px; 184 | color: #eee; 185 | margin: auto; 186 | text-decoration: none; 187 | padding-top: 10px; 188 | padding-bottom: 10px; 189 | padding-left: 10px; 190 | padding-right: 10px; 191 | width:200; 192 | border-radius:10; 193 | } 194 | .local { 195 | width:80; 196 | height:80; 197 | border-radius:60; 198 | } 199 | .name { 200 | font-size:180%; 201 | font-color:#444; 202 | font-family:helvetica; 203 | } 204 | .nearb { 205 | text-align:left; 206 | color:#777; 207 | font-size:140%; 208 | } 209 | .page-nav { 210 | margin-left:auto; 211 | margin-right:100; 212 | float:right; 213 | font-size:230%; 214 | color: #134db7; 215 | } 216 | .inline { 217 | display:inline-block; 218 | } 219 | .channel { 220 | font-size: 140%; 221 | color: #235dc7; 222 | padding-left:30px; 223 | } 224 | .mylightbox { 225 | display:none; 226 | width:100%; 227 | height:100%; 228 | background: rgba(0,0,0,0.6); 229 | position:fixed; 230 | z-index:999; 231 | text-align:center; 232 | top:0; 233 | left:0; 234 | } 235 | .mylightbox:target { 236 | display:block; 237 | } 238 | .pubwindow { 239 | display:none; 240 | width:100%; 241 | height:100%; 242 | background: rgba(0,0,0,0.0); 243 | position:fixed; 244 | z-index:999; 245 | text-align:center; 246 | top:0; 247 | left:0; 248 | } 249 | .pubwindow:target { 250 | display:block; 251 | } 252 | .pubforward { 253 | background: rgba(230,230,230,1.0); 254 | float:right; 255 | margin-right:30; 256 | #margin:auto; 257 | margin-top:50%; 258 | height:150; 259 | width:260; 260 | border-radius:10px; 261 | border:2px solid lightgrey; 262 | padding:30; 263 | } 264 | .bigbutton, bigbutton a { 265 | float:right; 266 | width:70; 267 | height:70; 268 | margin:auto; 269 | text-align:center; 270 | padding-top:23; 271 | background-color:#437de7; 272 | color:#fff; 273 | font-size:120%; 274 | position:fixed; 275 | right:20; 276 | border-radius:80px; 277 | bottom:20; 278 | } 279 | .mythread { 280 | margin:auto; 281 | margin-top:100; 282 | height:10000; 283 | width:640; 284 | border-radius:10px; 285 | border:0px; 286 | } 287 | .exit { 288 | color:#eee; 289 | text-decoration:none; 290 | font-size:300%; 291 | position:fixed; 292 | top:6; 293 | right:12; 294 | } 295 | .postbutton { 296 | float:right; 297 | padding:8px; 298 | color:white; 299 | background-color: #235dc7;; 300 | border:none; 301 | font-size:120%; 302 | margin-right:30; 303 | margin-left:30; 304 | border-radius:8px; 305 | } 306 | .postbutton:hover { 307 | background-color: #134db7; 308 | } 309 | .poster { 310 | width:366; 311 | height:90; 312 | border:none; 313 | resize:none; 314 | } 315 | .rawbutton { 316 | float:right; 317 | margin:4; 318 | color:darkgrey; 319 | } 320 | .mylink { 321 | margin:60; 322 | font-size:140%; 323 | margin-top:100; 324 | float:right; 325 | } 326 | -------------------------------------------------------------------------------- /cmd/sbot/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyleap/go-ssb/ca58dd26bae34ab054f86293e49a6f4d16a9aa15/cmd/sbot/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /cmd/sbot/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyleap/go-ssb/ca58dd26bae34ab054f86293e49a6f4d16a9aa15/cmd/sbot/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /cmd/sbot/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyleap/go-ssb/ca58dd26bae34ab054f86293e49a6f4d16a9aa15/cmd/sbot/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /cmd/sbot/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyleap/go-ssb/ca58dd26bae34ab054f86293e49a6f4d16a9aa15/cmd/sbot/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /cmd/sbot/templates/content/about.tpl: -------------------------------------------------------------------------------- 1 | {{Avatar .Message.Author}} 2 | {{if eq .Message.Author.String .Content.About.String}} 3 | self identifies as {{.Content.Name}} 4 | {{else}} 5 | identifies {{.Content.About}} as {{.Content.Name}} 6 | {{end}} -------------------------------------------------------------------------------- /cmd/sbot/templates/content/avatar.tpl: -------------------------------------------------------------------------------- 1 |
{{if .About}} 2 | {{if .About.Image}}
{{end}} 3 | {{.About.Name}} 4 | {{else}} 5 | {{.Ref}} 6 | {{end}}
7 | -------------------------------------------------------------------------------- /cmd/sbot/templates/content/channel.tpl: -------------------------------------------------------------------------------- 1 | {{Avatar .Message.Author}} 2 | {{if .Content.Subscribed}}subscribed to{{else}}unsubscribed from{{end}} #{{.Content.Channel}} -------------------------------------------------------------------------------- /cmd/sbot/templates/content/contact.tpl: -------------------------------------------------------------------------------- 1 | {{Avatar .Message.Author}}{{if .Content.Following}} followed {{else}} unfollowed {{end}} 2 | {{$contact := GetAbout .Content.Contact}} 3 | 4 | {{if $contact}} 5 | {{$contact.Name}} 6 | {{else}} 7 | {{.Content.Contact}} 8 | {{end}} 9 | 10 | {{RenderJSTime .Message.Timestamp}} 11 | {{$votes := GetVotes .Message.Key}}{{len $votes}} Votes 12 | -------------------------------------------------------------------------------- /cmd/sbot/templates/content/follow.tpl: -------------------------------------------------------------------------------- 1 | {{if .About}} 2 | {{if .About.Name}}{{.About.Name}} 3 | {{else}}{{.Ref}}{{end}} 4 | {{end}} 5 | -------------------------------------------------------------------------------- /cmd/sbot/templates/content/git-repo.tpl: -------------------------------------------------------------------------------- 1 | {{$about := GetAbout .Message.Key}}{{if $about}}{{.About.Name}}{{else}}{{.Message.Key}}{{end}} -------------------------------------------------------------------------------- /cmd/sbot/templates/content/git-update.tpl: -------------------------------------------------------------------------------- 1 |
2 |
{{Avatar .Message.Author}} pushed {{len .Content.Commits}} commits
3 |
4 |
5 | 8 |
9 | {{$votes := GetVotes .Message.Key}}{{len $votes}} Votes 10 |
11 |
12 | {{if .Content.Repo.IsMessage}}{{.Content.Repo}}{{end}} 13 | 14 |
15 |
16 |
17 | 18 | {{range .Content.Commits}} 19 |
20 | Commit {{.Sha1}}
21 | {{.Title}}
22 | {{.Body}} 23 | {{end}} 24 |
-------------------------------------------------------------------------------- /cmd/sbot/templates/content/issue.tpl: -------------------------------------------------------------------------------- 1 |
2 |
{{Avatar .Message.Author}}
3 |
4 |
5 | 8 |
9 | {{$votes := GetVotes .Message.Key}}{{len $votes}} Votes 10 |
11 |
12 | {{if .Content.Project.IsMessage}}{{.Content.Project}}{{end}} 13 | 14 |
15 |
16 |
17 | {{Markdown .Content.Text}} 18 |
19 | View comments
20 | -------------------------------------------------------------------------------- /cmd/sbot/templates/content/message.tpl: -------------------------------------------------------------------------------- 1 | {{if ge .Levels 0}} 2 | {{if .Content.Branch.IsMessage}} 3 | View full thread
4 |
5 | {{$prior := GetMessage .Content.Branch}}{{RenderContent $prior .Levels}} 6 |
7 | {{end}} 8 | {{end}} 9 |
10 |
{{Avatar .Message.Author}}
11 |
12 | {{RenderJSTime .Message.Timestamp}}
13 |
14 |
15 | {{if ne .Content.Channel ""}}#{{.Content.Channel}}{{end}} 16 |
17 |
18 | {{$votes := GetVotes .Message.Key}}{{len $votes}} Votes 19 |
20 |
21 |
22 |
23 |
24 | {{Markdown .Content.Text}} 25 |
26 | -------------------------------------------------------------------------------- /cmd/sbot/templates/content/post.tpl: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{Avatar .Message.Author}} 4 |
5 | 6 | 7 | {{if ne .Content.Channel ""}}#{{.Content.Channel}}{{end}} 8 | 9 |
10 | 11 |
12 | 13 | 31 | 32 | 33 |
34 | {{if ge .Levels 0}} 35 | {{if .Content.Branch.IsMessage}} 36 | 37 | 38 | View full thread 39 | 40 | 41 | 42 |
43 | 46 |
47 | 48 | {{$prior := GetMessage .Content.Branch}}{{RenderContent $prior .Levels}} 49 | {{end}} 50 | {{end}} 51 |
52 | {{Markdown .Content.Text}} 53 | 54 | 55 | json 56 | 57 | 58 |
59 | 60 |
61 | -------------------------------------------------------------------------------- /cmd/sbot/templates/content/vote-simple.tpl: -------------------------------------------------------------------------------- 1 | {{Avatar .Message.Author}} -------------------------------------------------------------------------------- /cmd/sbot/templates/content/vote.tpl: -------------------------------------------------------------------------------- 1 | {{Avatar .Message.Author}} 2 | {{if ne .Content.Vote.Reason ""}}{{.Content.Vote.Reason}}{{else}} 3 | {{if gt .Content.Vote.Value 0}}liked 4 | {{else if lt .Content.Vote.Value 0}}disliked 5 | {{else}}noted{{end}}{{end}} 6 | a post 7 |
8 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/addpub.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 |
8 | 9 |
10 | 14 |
15 | 16 |
17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/admin.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 | {{template "navbar.tpl"}} 8 | 9 | 10 | {{range $b, $size := .DBSize}} 11 | 12 | {{end}} 13 |
keysize
{{$b}}{{$size}}

14 | all 15 | {{range .Modules}} 16 | {{.}} 17 | {{end}} 18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 | 39 |
40 | 41 |
42 |
43 |
44 | 45 |
46 | 47 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/blob.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 | 8 | {{template "navbar.tpl"}} 9 | 10 |
11 | {{.ID}}
12 | 13 |
14 |
15 | 16 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/channel.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 | 8 | {{template "navbar.tpl"}} 9 | 10 |
11 |

12 | #{{.Channel}} 13 |

14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 | {{range .Messages}} 23 |
24 | {{RenderContent . 1}} 25 |
26 | {{end}} 27 | 28 | 29 |
30 | 40 |
41 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/feed.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 | {{template "navbar.tpl"}} 8 | 9 |
10 |
11 | {{if .Profile}} 12 | {{if .Profile.Image}}{{end}} 13 |
@{{.Profile.Name}}

{{.Ref}}

14 | 15 | 16 | 17 |
18 |
19 | {{else}} 20 | {{.Ref}} 21 | {{end}} 22 |
23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 |

34 | {{range $k, $v := .Follows}} 35 | {{Avatar $k}} 36 | {{end}} 37 |

38 | 39 |
40 |
41 | 42 | {{range .Messages}} 43 |
44 | {{RenderContent . 1}} 45 |
46 | {{end}} 47 |
48 | 49 |
50 | 60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/header.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 | 7 | {{template "navbar.tpl"}} 8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 | 16 |
17 | 18 | {{range .Messages}} 19 | {{RenderContent . 1}} 20 | {{end}} 21 | 22 | 23 | 28 | 29 | 30 |
31 | 34 |
35 | 36 | 37 |
38 | 48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/message.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 | 8 | {{template "navbar.tpl"}} 9 | 10 |
11 | {{RenderContent .Message 1}} 12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/navbar.tpl: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/post.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 | 8 | {{template "navbar.tpl"}} 9 | 10 |
11 | {{RenderContent .Message 1}} 12 |
13 | {{range .Votes}} 14 | {{$vote := Decode .}} 15 |
16 | {{RenderContentTemplate . 0 "vote-simple"}} 17 |
18 | {{end}} 19 |
20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | 33 |
34 | 35 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/profile.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 | 8 | {{template "navbar.tpl"}} 9 | 10 |
11 | {{if .Profile}} 12 | {{if .Profile.Image}}
{{end}} 13 |
14 |
15 | {{.Profile.Name}} 16 | {{else}} 17 | {{.Ref}} 18 | {{end}} 19 | {{.Ref}} 20 |
21 | 22 |
23 |
24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 |
32 | {{range .Messages}} 33 | {{RenderContent . 1}} 34 | {{end}} 35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/repo.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 | 8 | {{template "navbar.tpl"}} 9 | 10 |
11 | Want all blobs 12 |
13 | 14 |
15 | {{range .Issues}} 16 |
17 | {{RenderContent . 1}} 18 |
19 | {{end}} 20 |
21 | 22 |
23 | {{range .Blobs}} 24 | {{.}} - {{if HasBlob .}}Present{{else}}Not Present{{end}}
25 | {{end}} 26 |
27 | 28 |
29 | {{range .Updates}} 30 | {{.}}
31 | {{end}} 32 |
33 |
34 | 35 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/search.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 | 8 | {{template "navbar.tpl"}} 9 | 10 | {{range .Messages}} 11 |
12 | {{RenderContent . 1}} 13 |
14 | {{end}} 15 |
16 | 17 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/thread.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 | 7 |
8 | 9 |
10 | {{RenderContent .Root 0}} 11 |
12 | {{range .Messages}} 13 |
14 | {{RenderContent . 0}} 15 |
16 | {{end}} 17 | 18 | 19 |
20 |
21 | {{if .Profile}} 22 | {{if .Profile.Image}}
{{end}} 23 | {{.Profile.Name}} 24 | {{else}} 25 | {{.Ref}} 26 | {{end}} 27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 |
36 | 37 |
38 |
39 |
40 | 41 | Go home 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /cmd/sbot/templates/pages/upload.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "header.tpl"}} 4 | 5 | 6 |
7 | 8 | {{template "navbar.tpl"}} 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 |
17 | 18 | -------------------------------------------------------------------------------- /cmd/sbot/webui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "html/template" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | _ "net/http/pprof" 12 | "net/url" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/boltdb/bolt" 18 | "github.com/microcosm-cc/bluemonday" 19 | "github.com/russross/blackfriday" 20 | 21 | "github.com/andyleap/boltinspect" 22 | 23 | "github.com/andyleap/go-ssb" 24 | "github.com/andyleap/go-ssb/blobs" 25 | "github.com/andyleap/go-ssb/channels" 26 | "github.com/andyleap/go-ssb/git" 27 | "github.com/andyleap/go-ssb/gossip" 28 | "github.com/andyleap/go-ssb/graph" 29 | "github.com/andyleap/go-ssb/search" 30 | "github.com/andyleap/go-ssb/social" 31 | ) 32 | 33 | var ContentTemplates = template.New("content") 34 | 35 | type SSBRenderer struct { 36 | blackfriday.Renderer 37 | } 38 | 39 | func GetURL(link, name string) (string, string) { 40 | 41 | r := ssb.ParseRef(link) 42 | switch r.Type { 43 | case ssb.RefBlob: 44 | return "/blob?id=" + url.QueryEscape(r.String()), name 45 | case ssb.RefMessage: 46 | msg := datastore.Get(nil, r) 47 | if msg != nil { 48 | _, repo := msg.DecodeMessage() 49 | if _, ok := repo.(*git.RepoRoot); ok { 50 | return "/repo?id=" + url.QueryEscape(r.String()), name 51 | } 52 | } 53 | return "/post?id=" + url.QueryEscape(r.String()), name 54 | 55 | case ssb.RefFeed: 56 | a := &social.About{} 57 | datastore.DB().View(func(tx *bolt.Tx) error { 58 | a = social.GetAbout(tx, r) 59 | return nil 60 | }) 61 | if a != nil && a.Name != "" { 62 | name = "@" + a.Name 63 | } 64 | return "/feed?id=" + url.QueryEscape(r.String()), name 65 | } 66 | if link[0] == '#' { 67 | return "/channel?channel=" + url.QueryEscape(string(link[1:])), name 68 | } 69 | return link, name 70 | } 71 | 72 | func (ssbr *SSBRenderer) AutoLink(out *bytes.Buffer, link []byte, kind int) { 73 | url, _ := GetURL(string(link), "") 74 | ssbr.Renderer.AutoLink(out, []byte(url), kind) 75 | } 76 | func (ssbr *SSBRenderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { 77 | url, name := GetURL(string(link), string(content)) 78 | if name != string(content) { 79 | title = content 80 | } 81 | 82 | ssbr.Renderer.Link(out, []byte(url), title, []byte(name)) 83 | } 84 | 85 | func (ssbr *SSBRenderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { 86 | r := ssb.ParseRef(string(link)) 87 | switch r.Type { 88 | case ssb.RefBlob: 89 | link = []byte("/blob?id=" + url.QueryEscape(r.String())) 90 | } 91 | ssbr.Renderer.Image(out, link, title, alt) 92 | } 93 | 94 | func RenderMarkdown(input []byte) []byte { 95 | commonHtmlFlags := 0 | 96 | blackfriday.HTML_USE_XHTML | 97 | blackfriday.HTML_USE_SMARTYPANTS | 98 | blackfriday.HTML_SMARTYPANTS_FRACTIONS | 99 | blackfriday.HTML_SMARTYPANTS_DASHES | 100 | blackfriday.HTML_SMARTYPANTS_LATEX_DASHES 101 | 102 | commonExtensions := 0 | 103 | blackfriday.EXTENSION_NO_INTRA_EMPHASIS | 104 | blackfriday.EXTENSION_TABLES | 105 | blackfriday.EXTENSION_FENCED_CODE | 106 | blackfriday.EXTENSION_AUTOLINK | 107 | blackfriday.EXTENSION_STRIKETHROUGH | 108 | blackfriday.EXTENSION_SPACE_HEADERS | 109 | blackfriday.EXTENSION_HEADER_IDS | 110 | blackfriday.EXTENSION_BACKSLASH_LINE_BREAK | 111 | blackfriday.EXTENSION_DEFINITION_LISTS 112 | // set up the HTML renderer 113 | renderer := &SSBRenderer{blackfriday.HtmlRenderer(commonHtmlFlags, "", "")} 114 | 115 | options := blackfriday.Options{ 116 | Extensions: commonExtensions} 117 | 118 | pol := bluemonday.UGCPolicy() 119 | 120 | return pol.SanitizeBytes(blackfriday.MarkdownOptions(input, renderer, options)) 121 | } 122 | 123 | func init() { 124 | template.Must(ContentTemplates.Funcs(template.FuncMap{ 125 | "Avatar": func(ref ssb.Ref) template.HTML { 126 | if ref.Type != ssb.RefFeed { 127 | return "" 128 | } 129 | var a *social.About 130 | datastore.DB().View(func(tx *bolt.Tx) error { 131 | a = social.GetAbout(tx, ref) 132 | return nil 133 | }) 134 | buf := &bytes.Buffer{} 135 | err := ContentTemplates.ExecuteTemplate(buf, "avatar.tpl", struct { 136 | About *social.About 137 | Ref ssb.Ref 138 | }{a, ref}) 139 | if err != nil { 140 | log.Println(err) 141 | } 142 | return template.HTML(buf.String()) 143 | }, 144 | "GetAbout": func(ref ssb.Ref) (a *social.About) { 145 | datastore.DB().View(func(tx *bolt.Tx) error { 146 | a = social.GetAbout(tx, ref) 147 | return nil 148 | }) 149 | return 150 | }, 151 | "RenderJSTime": func(timestamp float64) string { 152 | t := time.Unix(0, int64(timestamp*float64(time.Millisecond))).Local() 153 | return t.Format(time.ANSIC) 154 | }, 155 | "Markdown": func(markdown string) template.HTML { 156 | return template.HTML(RenderMarkdown([]byte(markdown))) 157 | }, 158 | "GetMessage": func(ref ssb.Ref) *ssb.SignedMessage { 159 | return datastore.Get(nil, ref) 160 | }, 161 | "GetVotes": func(ref ssb.Ref) (votes []*ssb.SignedMessage) { 162 | datastore.DB().View(func(tx *bolt.Tx) error { 163 | votes = social.GetVotes(tx, ref) 164 | return nil 165 | }) 166 | return 167 | }, 168 | "HasBlob": func(ref ssb.Ref) bool { 169 | return blobs.Get(datastore).Has(ref) 170 | }, 171 | "RenderContent": func(m *ssb.SignedMessage, levels int) template.HTML { 172 | if m == nil { 173 | return "" 174 | } 175 | t, md := m.DecodeMessage() 176 | if t == "" { 177 | return template.HTML("") 178 | } 179 | buf := &bytes.Buffer{} 180 | tpl := ContentTemplates.Lookup(t + ".tpl") 181 | if tpl == nil { 182 | return template.HTML("
" + string(m.Encode()) + "
") 183 | } 184 | 185 | err := ContentTemplates.ExecuteTemplate(buf, t+".tpl", struct { 186 | Message *ssb.SignedMessage 187 | Content interface{} 188 | Levels int 189 | }{m, md, levels - 1}) 190 | if err != nil { 191 | log.Println(err) 192 | } 193 | return template.HTML(buf.String()) 194 | }, 195 | }).ParseGlob("templates/content/*.tpl")) 196 | } 197 | 198 | var PageTemplates = template.Must(template.New("index").Funcs(template.FuncMap{ 199 | "Avatar": func(ref ssb.Ref) template.HTML { 200 | if ref.Type != ssb.RefFeed { 201 | return "" 202 | } 203 | var a *social.About 204 | datastore.DB().View(func(tx *bolt.Tx) error { 205 | a = social.GetAbout(tx, ref) 206 | return nil 207 | }) 208 | buf := &bytes.Buffer{} 209 | err := ContentTemplates.ExecuteTemplate(buf, "follow.tpl", struct { 210 | About *social.About 211 | Ref ssb.Ref 212 | }{a, ref}) 213 | if err != nil { 214 | log.Println(err) 215 | } 216 | return template.HTML(buf.String()) 217 | }, 218 | "RenderContent": func(m *ssb.SignedMessage, levels int) template.HTML { 219 | t, md := m.DecodeMessage() 220 | if t == "" { 221 | return template.HTML("") 222 | } 223 | tpl := ContentTemplates.Lookup(t + ".tpl") 224 | if tpl == nil { 225 | return template.HTML("
" + string(m.Encode()) + "
") 226 | } 227 | buf := &bytes.Buffer{} 228 | err := ContentTemplates.ExecuteTemplate(buf, t+".tpl", struct { 229 | Message *ssb.SignedMessage 230 | Content interface{} 231 | Levels int 232 | }{m, md, levels - 1}) 233 | if err != nil { 234 | log.Println(err) 235 | } 236 | return template.HTML(buf.String()) 237 | }, 238 | "HasBlob": func(ref ssb.Ref) bool { 239 | return blobs.Get(datastore).Has(ref) 240 | }, 241 | "RenderContentTemplate": func(m *ssb.SignedMessage, levels int, tpl string) template.HTML { 242 | t, md := m.DecodeMessage() 243 | buf := &bytes.Buffer{} 244 | err := ContentTemplates.ExecuteTemplate(buf, tpl+".tpl", struct { 245 | Message *ssb.SignedMessage 246 | Content interface{} 247 | Levels int 248 | }{m, md, levels - 1}) 249 | if err != nil { 250 | log.Println(err) 251 | } 252 | return template.HTML(t + buf.String()) 253 | }, 254 | "Decode": func(m *ssb.SignedMessage) interface{} { 255 | _, mb := m.DecodeMessage() 256 | return mb 257 | }, 258 | }).ParseGlob("templates/pages/*.tpl")) 259 | 260 | func init() { 261 | log.Println(ContentTemplates.DefinedTemplates()) 262 | log.Println(PageTemplates.DefinedTemplates()) 263 | } 264 | 265 | func RegisterWebui() { 266 | bi := boltinspect.New(datastore.DB()) 267 | 268 | http.HandleFunc("/bolt", bi.InspectEndpoint) 269 | 270 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) 271 | 272 | http.HandleFunc("/", Index) 273 | http.Handle("/favicon.ico", http.NotFoundHandler()) 274 | http.HandleFunc("/channel", Channel) 275 | http.HandleFunc("/post", Post) 276 | http.HandleFunc("/search", Search) 277 | http.HandleFunc("/publish/post", PublishPost) 278 | http.HandleFunc("/publish/about", PublishAbout) 279 | http.HandleFunc("/publish/follow", PublishFollow) 280 | http.HandleFunc("/publish/vote", PublishVote) 281 | http.HandleFunc("/gossip/add", GossipAdd) 282 | http.HandleFunc("/gossip/accept", GossipAccept) 283 | 284 | http.HandleFunc("/feed", FeedPage) 285 | http.HandleFunc("/thread", ThreadPage) 286 | 287 | http.HandleFunc("/profile", Profile) 288 | 289 | http.HandleFunc("/admin", Admin) 290 | http.HandleFunc("/addpub", AddPub) 291 | http.HandleFunc("/rebuild", Rebuild) 292 | 293 | http.HandleFunc("/blob", Blob) 294 | http.HandleFunc("/blobinfo", BlobInfo) 295 | 296 | http.HandleFunc("/repo", RepoInfo) 297 | http.HandleFunc("/repo/want", RepoWant) 298 | 299 | http.HandleFunc("/raw", Raw) 300 | 301 | http.HandleFunc("/upload", Upload) 302 | 303 | go http.ListenAndServe("localhost:9823", nil) 304 | } 305 | 306 | func Upload(rw http.ResponseWriter, req *http.Request) { 307 | f, _, err := req.FormFile("upload") 308 | if err != nil { 309 | log.Println(err) 310 | PageTemplates.ExecuteTemplate(rw, "upload.tpl", nil) 311 | return 312 | } 313 | buf, _ := ioutil.ReadAll(f) 314 | bs := datastore.ExtraData("blobStore").(*blobs.BlobStore) 315 | ref := bs.Add(buf) 316 | http.Redirect(rw, req, "/blobinfo?id="+url.QueryEscape(ref.String()), http.StatusFound) 317 | } 318 | 319 | func PublishPost(rw http.ResponseWriter, req *http.Request) { 320 | p := &social.Post{} 321 | p.Type = "post" 322 | p.Root = ssb.ParseRef(req.FormValue("root")) 323 | p.Branch = ssb.ParseRef(req.FormValue("branch")) 324 | p.Channel = req.FormValue("channel") 325 | p.Text = req.FormValue("text") 326 | datastore.GetFeed(datastore.PrimaryRef).PublishMessage(p) 327 | http.Redirect(rw, req, req.FormValue("returnto"), http.StatusSeeOther) 328 | } 329 | 330 | func PublishVote(rw http.ResponseWriter, req *http.Request) { 331 | p := &social.Vote{} 332 | p.Type = "vote" 333 | p.Vote.Link = ssb.ParseRef(req.FormValue("link")) 334 | p.Vote.Value = 1 335 | p.Vote.Reason = "" 336 | datastore.GetFeed(datastore.PrimaryRef).PublishMessage(p) 337 | http.Redirect(rw, req, req.FormValue("returnto"), http.StatusSeeOther) 338 | } 339 | 340 | func PublishAbout(rw http.ResponseWriter, req *http.Request) { 341 | p := &social.About{} 342 | p.Type = "about" 343 | p.About = datastore.PrimaryRef 344 | p.Name = req.FormValue("name") 345 | f, _, err := req.FormFile("upload") 346 | if err == nil { 347 | buf, _ := ioutil.ReadAll(f) 348 | bs := datastore.ExtraData("blobStore").(*blobs.BlobStore) 349 | ref := bs.Add(buf) 350 | p.Image = &social.Image{} 351 | p.Image.Link = ref 352 | } 353 | datastore.GetFeed(datastore.PrimaryRef).PublishMessage(p) 354 | http.Redirect(rw, req, "/profile", http.StatusSeeOther) 355 | } 356 | 357 | func PublishFollow(rw http.ResponseWriter, req *http.Request) { 358 | feed := ssb.ParseRef(req.FormValue("feed")) 359 | if feed.Type == ssb.RefInvalid { 360 | http.Redirect(rw, req, req.FormValue("returnto"), http.StatusSeeOther) 361 | } 362 | p := &graph.Contact{} 363 | p.Type = "contact" 364 | p.Contact = feed 365 | following := true 366 | p.Following = &following 367 | datastore.GetFeed(datastore.PrimaryRef).PublishMessage(p) 368 | http.Redirect(rw, req, req.FormValue("returnto"), http.StatusSeeOther) 369 | } 370 | 371 | func GossipAdd(rw http.ResponseWriter, req *http.Request) { 372 | host := req.FormValue("host") 373 | if host == "" { 374 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 375 | return 376 | } 377 | portStr := req.FormValue("port") 378 | if portStr == "" { 379 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 380 | return 381 | } 382 | port, err := strconv.ParseInt(portStr, 10, 64) 383 | if err != nil { 384 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 385 | return 386 | } 387 | key := ssb.ParseRef(req.FormValue("key")) 388 | if key.Type != ssb.RefFeed { 389 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 390 | return 391 | } 392 | 393 | pub := gossip.Pub{ 394 | Host: host, 395 | Port: int(port), 396 | Link: key, 397 | } 398 | gossip.AddPub(datastore, pub) 399 | 400 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 401 | } 402 | 403 | func GossipAccept(rw http.ResponseWriter, req *http.Request) { 404 | invite := req.FormValue("invite") 405 | if invite == "" { 406 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 407 | return 408 | } 409 | parts := strings.Split(invite, "~") 410 | if len(parts) != 2 { 411 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 412 | return 413 | } 414 | addrparts := strings.Split(parts[0], ":") 415 | if len(addrparts) != 3 { 416 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 417 | return 418 | } 419 | port, err := strconv.ParseInt(addrparts[1], 10, 64) 420 | if err != nil { 421 | log.Println(err) 422 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 423 | return 424 | } 425 | follow := req.FormValue("follow") 426 | 427 | pub := gossip.Pub{ 428 | Host: addrparts[0], 429 | Port: int(port), 430 | Link: ssb.ParseRef(addrparts[2]), 431 | } 432 | 433 | seed, err := base64.StdEncoding.DecodeString(parts[1]) 434 | if err != nil { 435 | log.Println(err) 436 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 437 | return 438 | } 439 | 440 | err = gossip.AcceptInvite(datastore, pub, seed) 441 | 442 | if err != nil { 443 | log.Println(err) 444 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 445 | return 446 | } 447 | 448 | if follow == "follow" { 449 | p := &graph.Contact{} 450 | p.Type = "contact" 451 | p.Contact = pub.Link 452 | following := true 453 | p.Following = &following 454 | datastore.GetFeed(datastore.PrimaryRef).PublishMessage(p) 455 | } 456 | 457 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 458 | } 459 | 460 | func Rebuild(rw http.ResponseWriter, req *http.Request) { 461 | module := req.FormValue("module") 462 | if module != "" { 463 | if module == "all" { 464 | datastore.RebuildAll() 465 | } else { 466 | datastore.Rebuild(module) 467 | } 468 | } 469 | http.Redirect(rw, req, "/admin", http.StatusSeeOther) 470 | } 471 | 472 | func calcSize(tx *bolt.Tx, b *bolt.Bucket) (size int) { 473 | b.ForEach(func(k, v []byte) error { 474 | size += len(k) 475 | if v == nil { 476 | size += calcSize(tx, b.Bucket(k)) 477 | } else { 478 | size += len(v) 479 | } 480 | return nil 481 | }) 482 | return 483 | } 484 | 485 | func Admin(rw http.ResponseWriter, req *http.Request) { 486 | size := map[string]int{} 487 | datastore.DB().View(func(tx *bolt.Tx) error { 488 | tx.ForEach(func(k []byte, b *bolt.Bucket) error { 489 | size[string(k)] = calcSize(tx, b) 490 | return nil 491 | }) 492 | return nil 493 | }) 494 | 495 | modules := []string{} 496 | for module := range ssb.AddMessageHooks { 497 | modules = append(modules, module) 498 | } 499 | err := PageTemplates.ExecuteTemplate(rw, "admin.tpl", struct { 500 | Modules []string 501 | DBSize map[string]int 502 | }{ 503 | modules, 504 | size, 505 | }) 506 | if err != nil { 507 | log.Println(err) 508 | } 509 | } 510 | 511 | func AddPub(rw http.ResponseWriter, req *http.Request) { 512 | err := PageTemplates.ExecuteTemplate(rw, "addpub.tpl", struct { 513 | }{}) 514 | //does it matter that nothing is there? 515 | if err != nil { 516 | log.Println(err) 517 | } 518 | } 519 | 520 | func Index(rw http.ResponseWriter, req *http.Request) { 521 | pageStr := req.FormValue("page") 522 | if pageStr == "" { 523 | pageStr = "1" 524 | } 525 | i, err := strconv.Atoi(pageStr) 526 | if err != nil { 527 | log.Println(err) 528 | } 529 | nextPage := strconv.Itoa(i + 1) 530 | prevPage := strconv.Itoa(i - 1) 531 | p := i * 25 532 | distStr := req.FormValue("dist") 533 | if distStr == "" { 534 | distStr = "2" 535 | } 536 | dist, _ := strconv.ParseInt(distStr, 10, 64) 537 | var messages []*ssb.SignedMessage 538 | if dist == 0 { 539 | f := datastore.GetFeed(datastore.PrimaryRef) 540 | messages = f.LatestCount(int(p), 0) 541 | } else { 542 | messages = datastore.LatestCountFiltered(int(p), int(p-25), graph.GetFollows(datastore, datastore.PrimaryRef, int(dist))) 543 | } 544 | err = PageTemplates.ExecuteTemplate(rw, "index.tpl", struct { 545 | Messages []*ssb.SignedMessage 546 | PageStr string 547 | NextPage string 548 | PrevPage string 549 | }{ 550 | messages, 551 | pageStr, 552 | nextPage, 553 | prevPage, 554 | }) 555 | if err != nil { 556 | log.Println(err) 557 | } 558 | } 559 | 560 | func FeedPage(rw http.ResponseWriter, req *http.Request) { 561 | feedRaw := req.FormValue("id") 562 | feed := ssb.ParseRef(feedRaw) 563 | 564 | pageStr := req.FormValue("page") 565 | if pageStr == "" { 566 | pageStr = "1" 567 | } 568 | i, err := strconv.Atoi(pageStr) 569 | if err != nil { 570 | log.Println(err) 571 | } 572 | nextPage := strconv.Itoa(i + 1) 573 | prevPage := strconv.Itoa(i - 1) 574 | p := (i * 25) - 25 575 | 576 | var about *social.About 577 | datastore.DB().View(func(tx *bolt.Tx) error { 578 | about = social.GetAbout(tx, feed) 579 | return nil 580 | }) 581 | var messages []*ssb.SignedMessage 582 | f := datastore.GetFeed(feed) 583 | messages = f.LatestCount(25, p) 584 | // messages = datastore.LatestCountFiltered(25, 0, graph.GetFollows(datastore, feed, int(dist))) 585 | 586 | follows := graph.GetFollows(datastore, feed, 1) 587 | 588 | err = PageTemplates.ExecuteTemplate(rw, "feed.tpl", struct { 589 | Messages []*ssb.SignedMessage 590 | Profile *social.About 591 | Ref ssb.Ref 592 | PageStr string 593 | NextPage string 594 | PrevPage string 595 | Follows map[ssb.Ref]int 596 | }{ 597 | messages, 598 | about, 599 | feed, 600 | pageStr, 601 | nextPage, 602 | prevPage, 603 | follows, 604 | }) 605 | if err != nil { 606 | log.Println(err) 607 | } 608 | } 609 | 610 | func ThreadPage(rw http.ResponseWriter, req *http.Request) { 611 | threadRaw := req.FormValue("id") 612 | threadRef := ssb.ParseRef(threadRaw) 613 | 614 | root := datastore.Get(nil, threadRef) 615 | 616 | channel := "" 617 | 618 | _, p := root.DecodeMessage() 619 | 620 | if post, ok := p.(*social.Post); ok { 621 | channel = post.Channel 622 | } 623 | var messages []*ssb.SignedMessage 624 | datastore.DB().View(func(tx *bolt.Tx) error { 625 | messages = social.GetThread(tx, threadRef) 626 | return nil 627 | }) 628 | 629 | feedRaw := datastore.PrimaryRef.String() 630 | feed := ssb.ParseRef(feedRaw) 631 | 632 | var about *social.About 633 | datastore.DB().View(func(tx *bolt.Tx) error { 634 | about = social.GetAbout(tx, feed) 635 | return nil 636 | }) 637 | reply := root.Key() 638 | if len(messages) > 0 { 639 | reply = messages[len(messages)-1].Key() 640 | } 641 | 642 | err := PageTemplates.ExecuteTemplate(rw, "thread.tpl", struct { 643 | Root *ssb.SignedMessage 644 | Channel string 645 | Reply ssb.Ref 646 | Messages []*ssb.SignedMessage 647 | Profile *social.About 648 | }{ 649 | root, 650 | channel, 651 | reply, 652 | messages, 653 | about, 654 | }) 655 | if err != nil { 656 | log.Println(err) 657 | } 658 | } 659 | 660 | func Post(rw http.ResponseWriter, req *http.Request) { 661 | post := req.FormValue("id") 662 | if post == "" { 663 | http.NotFound(rw, req) 664 | return 665 | } 666 | message := datastore.Get(nil, ssb.ParseRef(post)) 667 | if message == nil { 668 | http.NotFound(rw, req) 669 | return 670 | } 671 | _, content := message.DecodeMessage() 672 | raw := message.Encode() 673 | p, ok := content.(*social.Post) 674 | if !ok { 675 | PageTemplates.ExecuteTemplate(rw, "message.tpl", struct { 676 | Message *ssb.SignedMessage 677 | }{ 678 | message, 679 | }) 680 | return 681 | } 682 | var votes []*ssb.SignedMessage 683 | datastore.DB().View(func(tx *bolt.Tx) error { 684 | votes = social.GetVotes(tx, message.Key()) 685 | return nil 686 | }) 687 | err := PageTemplates.ExecuteTemplate(rw, "post.tpl", struct { 688 | Message *ssb.SignedMessage 689 | Content *social.Post 690 | Votes []*ssb.SignedMessage 691 | Raw []byte 692 | }{ 693 | message, 694 | p, 695 | votes, 696 | raw, 697 | }) 698 | if err != nil { 699 | log.Println(err) 700 | } 701 | } 702 | 703 | func Profile(rw http.ResponseWriter, req *http.Request) { 704 | feedRaw := datastore.PrimaryRef.String() 705 | distStr := req.FormValue("dist") 706 | if distStr == "" { 707 | distStr = "0" 708 | } 709 | feed := ssb.ParseRef(feedRaw) 710 | dist, _ := strconv.ParseInt(distStr, 10, 64) 711 | 712 | var about *social.About 713 | datastore.DB().View(func(tx *bolt.Tx) error { 714 | about = social.GetAbout(tx, feed) 715 | return nil 716 | }) 717 | var messages []*ssb.SignedMessage 718 | if dist == 0 { 719 | f := datastore.GetFeed(feed) 720 | messages = f.LatestCount(25, 0) 721 | } else { 722 | messages = datastore.LatestCountFiltered(25, 0, graph.GetFollows(datastore, feed, int(dist))) 723 | } 724 | err := PageTemplates.ExecuteTemplate(rw, "profile.tpl", struct { 725 | Messages []*ssb.SignedMessage 726 | Profile *social.About 727 | Ref ssb.Ref 728 | }{ 729 | messages, 730 | about, 731 | feed, 732 | }) 733 | if err != nil { 734 | log.Println(err) 735 | } 736 | } 737 | 738 | func Channel(rw http.ResponseWriter, req *http.Request) { 739 | channel := req.FormValue("channel") 740 | if channel == "" { 741 | Index(rw, req) 742 | return 743 | } 744 | pageStr := req.FormValue("page") 745 | if pageStr == "" { 746 | pageStr = "1" 747 | } 748 | i, err := strconv.Atoi(pageStr) 749 | if err != nil { 750 | log.Println(err) 751 | } 752 | nextPage := strconv.Itoa(i + 1) 753 | prevPage := strconv.Itoa(i - 1) 754 | p := i * 25 755 | messages := channels.GetChannelLatest(datastore, channel, int(p), int(p-24)) 756 | //set back to 100 posts per page^^ 757 | //this can be changed to support page loads with arbitrary slices from channel posts bucket 758 | //that zero is the start value 759 | err = PageTemplates.ExecuteTemplate(rw, "channel.tpl", struct { 760 | Messages []*ssb.SignedMessage 761 | Channel string 762 | PageStr string 763 | NextPage string 764 | PrevPage string 765 | }{ 766 | messages, 767 | channel, 768 | pageStr, 769 | nextPage, 770 | prevPage, 771 | }) 772 | if err != nil { 773 | log.Println(err) 774 | } 775 | } 776 | 777 | func Search(rw http.ResponseWriter, req *http.Request) { 778 | query := req.FormValue("q") 779 | if query == "" { 780 | Index(rw, req) 781 | return 782 | } 783 | if query[0] == '#' { 784 | http.Redirect(rw, req, "/channel?channel="+query[1:], http.StatusFound) 785 | return 786 | } 787 | r := ssb.ParseRef(query) 788 | switch r.Type { 789 | case ssb.RefBlob: 790 | http.Redirect(rw, req, "/blob?id="+url.QueryEscape(r.String()), http.StatusFound) 791 | return 792 | case ssb.RefMessage: 793 | msg := datastore.Get(nil, r) 794 | if msg != nil { 795 | _, repo := msg.DecodeMessage() 796 | if _, ok := repo.(*git.RepoRoot); ok { 797 | http.Redirect(rw, req, "/repo?id="+url.QueryEscape(r.String()), http.StatusFound) 798 | return 799 | } 800 | } 801 | http.Redirect(rw, req, "/post?id="+url.QueryEscape(r.String()), http.StatusFound) 802 | return 803 | case ssb.RefFeed: 804 | http.Redirect(rw, req, "/feed?id="+url.QueryEscape(r.String()), http.StatusFound) 805 | return 806 | } 807 | 808 | messages := search.Search(datastore, query, 50) 809 | err := PageTemplates.ExecuteTemplate(rw, "search.tpl", struct { 810 | Messages []*ssb.SignedMessage 811 | }{ 812 | messages, 813 | }) 814 | if err != nil { 815 | log.Println(err) 816 | } 817 | } 818 | 819 | func Blob(rw http.ResponseWriter, req *http.Request) { 820 | id := req.FormValue("id") 821 | if id == "" { 822 | http.NotFound(rw, req) 823 | return 824 | } 825 | r := ssb.ParseRef(id) 826 | bs := datastore.ExtraData("blobStore").(*blobs.BlobStore) 827 | if !bs.Has(r) { 828 | bs.Want(r) 829 | bs.WaitFor(r) 830 | } 831 | rc := bs.Get(r) 832 | defer rc.Close() 833 | rw.Header().Set("Cache-Control", "max-age=31556926") 834 | io.Copy(rw, rc) 835 | } 836 | 837 | func BlobInfo(rw http.ResponseWriter, req *http.Request) { 838 | id := req.FormValue("id") 839 | if id == "" { 840 | http.NotFound(rw, req) 841 | return 842 | } 843 | r := ssb.ParseRef(id) 844 | PageTemplates.ExecuteTemplate(rw, "blob.tpl", struct { 845 | ID ssb.Ref 846 | }{ 847 | ID: r, 848 | }) 849 | } 850 | 851 | func Raw(rw http.ResponseWriter, req *http.Request) { 852 | id := req.FormValue("id") 853 | if id == "" { 854 | http.NotFound(rw, req) 855 | return 856 | } 857 | r := ssb.ParseRef(id) 858 | m := datastore.Get(nil, r) 859 | if m != nil { 860 | buf := m.Encode() 861 | rw.Write(buf) 862 | } 863 | } 864 | 865 | func RepoInfo(rw http.ResponseWriter, req *http.Request) { 866 | id := req.FormValue("id") 867 | if id == "" { 868 | http.NotFound(rw, req) 869 | return 870 | } 871 | r := ssb.ParseRef(id) 872 | repo := git.Get(datastore, r) 873 | if repo == nil { 874 | http.NotFound(rw, req) 875 | return 876 | } 877 | bs := repo.ListBlobs() 878 | updates := repo.ListUpdates() 879 | issues := repo.Issues() 880 | err := PageTemplates.ExecuteTemplate(rw, "repo.tpl", struct { 881 | Blobs []ssb.Ref 882 | Updates []ssb.Ref 883 | Issues []*ssb.SignedMessage 884 | Ref ssb.Ref 885 | }{ 886 | bs, 887 | updates, 888 | issues, 889 | r, 890 | }) 891 | if err != nil { 892 | log.Println(err) 893 | } 894 | } 895 | 896 | func RepoWant(rw http.ResponseWriter, req *http.Request) { 897 | id := req.FormValue("id") 898 | if id == "" { 899 | http.NotFound(rw, req) 900 | return 901 | } 902 | r := ssb.ParseRef(id) 903 | repo := git.Get(datastore, r) 904 | if repo == nil { 905 | http.NotFound(rw, req) 906 | return 907 | } 908 | repo.WantAll() 909 | http.Redirect(rw, req, "/repo?id="+url.QueryEscape(r.String()), http.StatusFound) 910 | } 911 | -------------------------------------------------------------------------------- /cmd/sbotcli/.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | -------------------------------------------------------------------------------- /cmd/sbotcli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/urfave/cli" 9 | 10 | r "net/rpc" 11 | 12 | "github.com/andyleap/go-ssb/cmd/sbot/rpc" 13 | ) 14 | 15 | func main() { 16 | app := cli.NewApp() 17 | 18 | client, _ := r.Dial("tcp", "127.0.0.1:9822") 19 | 20 | app.Commands = []cli.Command{ 21 | { 22 | Name: "gossip.add", 23 | Aliases: []string{"g.a"}, 24 | Usage: "add a peer to the gossip list", 25 | Action: func(c *cli.Context) error { 26 | if c.NArg() != 3 { 27 | return fmt.Errorf("Expected 3 arguments") 28 | } 29 | port, err := strconv.Atoi(c.Args().Get(1)) 30 | if err != nil { 31 | return err 32 | } 33 | req := rpc.AddPubReq{ 34 | Host: c.Args().Get(0), 35 | Port: port, 36 | PubKey: c.Args().Get(2), 37 | } 38 | res := rpc.AddPubRes{} 39 | return client.Call("Gossip.AddPub", req, &res) 40 | }, 41 | }, 42 | { 43 | Name: "feed.post", 44 | Aliases: []string{"f.p"}, 45 | Usage: "publish a new post to a feed", 46 | Flags: []cli.Flag{ 47 | cli.StringFlag{ 48 | Name: "feed", 49 | Usage: "Feed to publish to", 50 | }, 51 | cli.StringFlag{ 52 | Name: "root", 53 | Usage: "Root post to respond to", 54 | }, 55 | cli.StringFlag{ 56 | Name: "branch", 57 | Usage: "Branch post to respond to", 58 | }, 59 | cli.StringFlag{ 60 | Name: "channel", 61 | Usage: "Channel to publish to", 62 | }, 63 | }, 64 | Action: func(c *cli.Context) error { 65 | if c.NArg() != 1 { 66 | return fmt.Errorf("Expected 1 argument") 67 | } 68 | req := rpc.PostReq{ 69 | Feed: c.String("feed"), 70 | Text: c.Args().Get(0), 71 | Root: c.String("root"), 72 | Branch: c.String("branch"), 73 | Channel: c.String("channel"), 74 | } 75 | res := rpc.PostRes{} 76 | return client.Call("Feed.Post", req, &res) 77 | }, 78 | }, 79 | { 80 | Name: "feed.follow", 81 | Aliases: []string{"f.f"}, 82 | Usage: "follow a feed", 83 | Flags: []cli.Flag{ 84 | cli.StringFlag{ 85 | Name: "feed", 86 | Usage: "Feed to publish to", 87 | }, 88 | }, 89 | Action: func(c *cli.Context) error { 90 | if c.NArg() != 1 { 91 | return fmt.Errorf("Expected 1 argument") 92 | } 93 | req := rpc.FollowReq{ 94 | Feed: c.String("feed"), 95 | Contact: c.Args().Get(0), 96 | } 97 | res := rpc.FollowRes{} 98 | return client.Call("Feed.Follow", req, &res) 99 | }, 100 | }, 101 | { 102 | Name: "feed.name", 103 | Aliases: []string{"f.n"}, 104 | Usage: "set a name for the feed", 105 | Flags: []cli.Flag{ 106 | cli.StringFlag{ 107 | Name: "feed", 108 | Usage: "Feed to publish to", 109 | }, 110 | }, 111 | Action: func(c *cli.Context) error { 112 | if c.NArg() != 1 { 113 | return fmt.Errorf("Expected 1 argument") 114 | } 115 | req := rpc.AboutReq{ 116 | Feed: c.String("feed"), 117 | Name: c.Args().Get(0), 118 | } 119 | res := rpc.AboutRes{} 120 | return client.Call("Feed.About", req, &res) 121 | }, 122 | }, 123 | } 124 | app.Run(os.Args) 125 | } 126 | -------------------------------------------------------------------------------- /compression.go: -------------------------------------------------------------------------------- 1 | package ssb 2 | 3 | var Compression1 = []byte(`{ 4 | "previous": ", 5 | "author": ", 6 | "sequence": , 7 | "timestamp": , 8 | "hash": "sha256", 9 | "content": { 10 | "type": "about"contact"post"vote", 11 | "text": ", 12 | "channel": ", 13 | "root": ", 14 | "branch": ", 15 | "recps": ", 16 | "mentions": [], 17 | "about": ", 18 | "name": ", 19 | "image": ", 20 | "contact": ", 21 | "following": true, 22 | "blocking": false, 23 | "vote": { 24 | "link": ", 25 | "value": 1, 26 | "reason": "Dug" 27 | } 28 | "repo": ", 29 | } 30 | "signature: ", 31 | }`) 32 | 33 | var Compression2 = []byte(`{ 34 | "previous": ", 35 | "author": ", 36 | "sequence": , 37 | "timestamp": , 38 | "hash": "sha256", 39 | "content": { 40 | "type": "about"contact"post"vote", 41 | "text": ", 42 | "channel": ", 43 | "root": ", 44 | "branch": ", 45 | "recps": ", 46 | "mentions": [], 47 | "about": ", 48 | "name": ", 49 | "image": ", 50 | "contact": ", 51 | "following": true, 52 | "blocking": false, 53 | "vote": { 54 | "link": ", 55 | "value": 1, 56 | "reason": "Dug" 57 | } 58 | "repo": " 59 | } 60 | "signature: " 61 | }`) 62 | -------------------------------------------------------------------------------- /dns/dns.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/boltdb/bolt" 7 | 8 | "github.com/andyleap/go-ssb" 9 | ) 10 | 11 | type Record struct { 12 | Name string `json:"name"` 13 | Class string `json:"class"` 14 | Type string `json:"type"` 15 | Data json.RawMessage `json:"data"` 16 | } 17 | 18 | type DNS struct { 19 | ssb.MessageBody 20 | Record Record `json:"record"` 21 | Branch []ssb.Ref `json:"branch"` 22 | } 23 | 24 | func init() { 25 | ssb.MessageTypes["ssb-dns"] = func() interface{} { return &DNS{} } 26 | ssb.RebuildClearHooks["dns"] = func(tx *bolt.Tx) error { 27 | tx.DeleteBucket([]byte("dns")) 28 | } 29 | ssb.AddMessageHooks["dns"] = func(m *ssb.SignedMessage, tx *bolt.Tx) error { 30 | _, mb := m.DecodeMessage() 31 | if mbr, ok := mb.(*DNS); ok { 32 | PubBucket, err := tx.CreateBucketIfNotExists([]byte("dns")) 33 | if err != nil { 34 | return err 35 | } 36 | buf, _ := json.Marshal(mbr) 37 | err = PubBucket.Put([]byte(m.Key()), buf) 38 | if err != nil { 39 | return err 40 | } 41 | return nil 42 | } 43 | return nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // go-ssb project doc.go 2 | 3 | /* 4 | go-ssb document 5 | */ 6 | package ssb 7 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | package ssb 2 | 3 | import ( 4 | "unicode/utf16" 5 | "unicode/utf8" 6 | 7 | "golang.org/x/text/encoding" 8 | "golang.org/x/text/transform" 9 | ) 10 | 11 | func RemoveUnsupported(e *encoding.Encoder) *encoding.Encoder { 12 | return &encoding.Encoder{Transformer: &errorHandler{e, errorToRemove}} 13 | } 14 | 15 | type errorHandler struct { 16 | *encoding.Encoder 17 | handler func(dst []byte, r rune, err repertoireError) (n int, ok bool) 18 | } 19 | 20 | // TODO: consider making this error public in some form. 21 | type repertoireError interface { 22 | Replacement() byte 23 | } 24 | 25 | func (h errorHandler) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { 26 | nDst, nSrc, err = h.Transformer.Transform(dst, src, atEOF) 27 | for err != nil { 28 | rerr, ok := err.(repertoireError) 29 | if !ok { 30 | return nDst, nSrc, err 31 | } 32 | r, sz := utf8.DecodeRune(src[nSrc:]) 33 | n, ok := h.handler(dst[nDst:], r, rerr) 34 | if !ok { 35 | return nDst, nSrc, transform.ErrShortDst 36 | } 37 | err = nil 38 | nDst += n 39 | if nSrc += sz; nSrc < len(src) { 40 | var dn, sn int 41 | dn, sn, err = h.Transformer.Transform(dst[nDst:], src[nSrc:], atEOF) 42 | nDst += dn 43 | nSrc += sn 44 | } 45 | } 46 | return nDst, nSrc, err 47 | } 48 | 49 | func errorToRemove(dst []byte, r rune, err repertoireError) (n int, ok bool) { 50 | if len(dst) < 1 { 51 | return 0, false 52 | } 53 | dst[0] = byte(r) 54 | return 1, true 55 | } 56 | 57 | func ToJSBinary(src []byte) []byte { 58 | runes := []rune(string(src)) 59 | utf := utf16.Encode(runes) 60 | out := make([]byte, len(utf)) 61 | for i, r := range utf { 62 | out[i] = byte(r) 63 | } 64 | return out 65 | } 66 | -------------------------------------------------------------------------------- /feed_test.go: -------------------------------------------------------------------------------- 1 | package ssb 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/user" 9 | "path/filepath" 10 | "testing" 11 | "time" 12 | 13 | "cryptoscope.co/go/secretstream" 14 | "cryptoscope.co/go/secretstream/secrethandshake" 15 | "github.com/cryptix/go-muxrpc" 16 | "github.com/go-kit/kit/log" 17 | ) 18 | 19 | func TestSlurp(t *testing.T) { 20 | sbotAppKey, _ := base64.StdEncoding.DecodeString("1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=") 21 | u, _ := user.Current() 22 | localKey, err := secrethandshake.LoadSSBKeyPair(filepath.Join(u.HomeDir, ".ssb", "secret")) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | var conn net.Conn 27 | c, err := secretstream.NewClient(*localKey, sbotAppKey) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | var remotPubKey = localKey.Public 32 | d, err := c.NewDialer(remotPubKey) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | fmt.Println("Connecting to local sbot") 37 | conn, err = d("tcp", "127.0.0.1:8008") 38 | fmt.Println("Connected to local sbot") 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout)) 43 | 44 | client := muxrpc.NewClient(logger, conn) 45 | 46 | output := make(chan *SignedMessage) 47 | 48 | fmt.Println("@" + base64.StdEncoding.EncodeToString(localKey.Public[:]) + ".ed25519") 49 | os.Remove("feeds.db") 50 | fmt.Println("test1") 51 | feedstore, _ := OpenDataStore("feeds.db", filepath.Join(u.HomeDir, ".ssb", "secret")) 52 | fmt.Println("test2") 53 | f := feedstore.GetFeed(Ref("@" + base64.StdEncoding.EncodeToString(localKey.Public[:]) + ".ed25519")) 54 | fmt.Println("test3") 55 | seq := 0 56 | 57 | latest := f.Latest() 58 | 59 | if latest != nil { 60 | seq = latest.Sequence + 1 61 | } 62 | 63 | go func() { 64 | client.Source("createHistoryStream", output, map[string]interface{}{"id": "@" + base64.StdEncoding.EncodeToString(localKey.Public[:]) + ".ed25519", "keys": false, "seq": seq}, 0, false) 65 | close(output) 66 | }() 67 | 68 | for m := range output { 69 | fmt.Println(m) 70 | err = f.AddMessage(m) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | } 75 | time.Sleep(120 * time.Second) 76 | } 77 | -------------------------------------------------------------------------------- /feeds.go: -------------------------------------------------------------------------------- 1 | package ssb 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "sync" 9 | "time" 10 | 11 | "cryptoscope.co/go/secretstream/secrethandshake" 12 | "github.com/boltdb/bolt" 13 | "golang.org/x/crypto/ed25519" 14 | ) 15 | 16 | func itob(v int) []byte { 17 | b := make([]byte, 8) 18 | binary.BigEndian.PutUint64(b, uint64(v)) 19 | return b 20 | } 21 | 22 | func btoi(b []byte) int { 23 | return int(binary.BigEndian.Uint64(b)) 24 | } 25 | 26 | var initMethods = []func(ds *DataStore){} 27 | 28 | func RegisterInit(f func(ds *DataStore)) { 29 | initMethods = append(initMethods, f) 30 | } 31 | 32 | type DataStore struct { 33 | db *bolt.DB 34 | 35 | feedlock sync.Mutex 36 | feeds map[Ref]*Feed 37 | 38 | Topic *MessageTopic 39 | 40 | PrimaryKey *secrethandshake.EdKeyPair 41 | PrimaryRef Ref 42 | 43 | extraData map[string]interface{} 44 | extraDataLock sync.Mutex 45 | 46 | Keys map[Ref]Signer 47 | } 48 | 49 | func init() { 50 | RegisterInit(func(ds *DataStore) { 51 | ds.RegisterMethod("feed.Publish", func(feed Ref, message interface{}) error { 52 | return ds.GetFeed(feed).PublishMessage(message) 53 | }) 54 | ds.RegisterMethod("feed.Latest", func(feed Ref) *SignedMessage { 55 | return ds.GetFeed(feed).Latest() 56 | }) 57 | }) 58 | /*AddMessageHooks["recompress"] = func(m *SignedMessage, tx *bolt.Tx) error { 59 | FeedsBucket, err := tx.CreateBucketIfNotExists([]byte("feeds")) 60 | if err != nil { 61 | return err 62 | } 63 | FeedBucket, err := FeedsBucket.CreateBucketIfNotExists(m.Author.DBKey()) 64 | if err != nil { 65 | return err 66 | } 67 | FeedLogBucket, err := FeedBucket.CreateBucketIfNotExists([]byte("log")) 68 | if err != nil { 69 | return err 70 | } 71 | FeedLogBucket.FillPercent = 1 72 | buf := m.Compress() 73 | err = FeedLogBucket.Put(itob(m.Sequence), buf) 74 | if err != nil { 75 | return err 76 | } 77 | return nil 78 | }*/ 79 | } 80 | 81 | func (ds *DataStore) RegisterMethod(name string, method interface{}) { 82 | RPCMethods, ok := ds.ExtraData("RPCMethods").(map[string]interface{}) 83 | if !ok { 84 | RPCMethods = map[string]interface{}{} 85 | } 86 | RPCMethods[name] = method 87 | ds.SetExtraData("RPCMethods", RPCMethods) 88 | } 89 | 90 | func (ds *DataStore) ExtraData(name string) interface{} { 91 | ds.extraDataLock.Lock() 92 | defer ds.extraDataLock.Unlock() 93 | return ds.extraData[name] 94 | } 95 | 96 | func (ds *DataStore) SetExtraData(name string, data interface{}) { 97 | ds.extraDataLock.Lock() 98 | defer ds.extraDataLock.Unlock() 99 | ds.extraData[name] = data 100 | } 101 | 102 | func (ds *DataStore) DB() *bolt.DB { 103 | return ds.db 104 | } 105 | 106 | func (ds *DataStore) Close() { 107 | err := ds.db.Close() 108 | if err != nil { 109 | log.Println("error closing db:", err) 110 | } 111 | } 112 | 113 | type Feed struct { 114 | store *DataStore 115 | ID Ref 116 | 117 | Topic *MessageTopic 118 | LatestSeq int 119 | SeqLock sync.Mutex 120 | 121 | addChan chan *SignedMessage 122 | 123 | waiting map[int]*SignedMessage 124 | waitingLock sync.Mutex 125 | waitingSignal *sync.Cond 126 | } 127 | 128 | type Pointer struct { 129 | Sequence int 130 | LogKey int 131 | Author []byte 132 | } 133 | 134 | func (p Pointer) Marshal() []byte { 135 | buf := make([]byte, len(p.Author)+16) 136 | binary.BigEndian.PutUint64(buf[0:], uint64(p.Sequence)) 137 | binary.BigEndian.PutUint64(buf[8:], uint64(p.LogKey)) 138 | copy(buf[16:], p.Author) 139 | return buf 140 | } 141 | 142 | func (p *Pointer) Unmarshal(buf []byte) { 143 | p.Author = make([]byte, len(buf)-16) 144 | p.Sequence = int(binary.BigEndian.Uint64(buf[0:])) 145 | p.LogKey = int(binary.BigEndian.Uint64(buf[8:])) 146 | copy(p.Author, buf[16:]) 147 | } 148 | 149 | func OpenDataStore(path string, primaryKey *secrethandshake.EdKeyPair) (*DataStore, error) { 150 | db, err := bolt.Open(path, 0600, nil) 151 | if err != nil { 152 | return nil, err 153 | } 154 | ds := &DataStore{ 155 | db: db, 156 | feeds: map[Ref]*Feed{}, 157 | Topic: NewMessageTopic(), 158 | extraData: map[string]interface{}{}, 159 | Keys: map[Ref]Signer{}, 160 | } 161 | ds.PrimaryKey = primaryKey 162 | ds.PrimaryRef, _ = NewRef(RefFeed, ds.PrimaryKey.Public[:], RefAlgoEd25519) 163 | ds.Keys[ds.PrimaryRef] = &SignerEd25519{ed25519.PrivateKey(ds.PrimaryKey.Secret[:])} 164 | 165 | for _, im := range initMethods { 166 | im(ds) 167 | } 168 | 169 | return ds, nil 170 | } 171 | 172 | func (ds *DataStore) GetFeed(feedID Ref) *Feed { 173 | ds.feedlock.Lock() 174 | defer ds.feedlock.Unlock() 175 | if feed, ok := ds.feeds[feedID]; ok { 176 | return feed 177 | } 178 | if feedID.Type != RefFeed { 179 | return nil 180 | } 181 | feed := &Feed{ 182 | store: ds, 183 | ID: feedID, 184 | Topic: NewMessageTopic(), 185 | addChan: make(chan *SignedMessage, 10), 186 | waiting: map[int]*SignedMessage{}, 187 | waitingSignal: sync.NewCond(&sync.Mutex{}), 188 | } 189 | go func() { 190 | for m := range feed.addChan { 191 | feed.waitingLock.Lock() 192 | feed.SeqLock.Lock() 193 | if m.Sequence > feed.LatestSeq { 194 | feed.waiting[m.Sequence] = m 195 | } 196 | feed.SeqLock.Unlock() 197 | feed.waitingLock.Unlock() 198 | feed.waitingSignal.Broadcast() 199 | } 200 | }() 201 | go feed.processMessageQueue() 202 | m := feed.Latest() 203 | if m != nil { 204 | feed.LatestSeq = m.Sequence 205 | } 206 | 207 | feed.Topic.Register(ds.Topic.Send, true) 208 | ds.feeds[feedID] = feed 209 | return feed 210 | } 211 | 212 | func (ds *DataStore) Get(tx *bolt.Tx, post Ref) (m *SignedMessage) { 213 | var err error 214 | if tx == nil { 215 | tx, err = ds.db.Begin(false) 216 | if err != nil { 217 | return 218 | } 219 | defer tx.Rollback() 220 | } 221 | PointerBucket := tx.Bucket([]byte("pointer")) 222 | if PointerBucket == nil { 223 | return 224 | } 225 | pdata := PointerBucket.Get(post.DBKey()) 226 | if pdata == nil { 227 | return 228 | } 229 | p := Pointer{} 230 | p.Unmarshal(pdata) 231 | FeedsBucket := tx.Bucket([]byte("feeds")) 232 | if FeedsBucket == nil { 233 | return 234 | } 235 | FeedBucket := FeedsBucket.Bucket(p.Author) 236 | if FeedBucket == nil { 237 | return 238 | } 239 | LogBucket := FeedBucket.Bucket([]byte("log")) 240 | if LogBucket == nil { 241 | return 242 | } 243 | msgdata := LogBucket.Get(itob(p.Sequence)) 244 | if msgdata == nil { 245 | return 246 | } 247 | m = DecompressMessage(msgdata) 248 | return 249 | } 250 | 251 | func GetMsg(tx *bolt.Tx, post Ref) (m *SignedMessage) { 252 | PointerBucket := tx.Bucket([]byte("pointer")) 253 | if PointerBucket == nil { 254 | return 255 | } 256 | pdata := PointerBucket.Get(post.DBKey()) 257 | if pdata == nil { 258 | return 259 | } 260 | p := Pointer{} 261 | p.Unmarshal(pdata) 262 | FeedsBucket := tx.Bucket([]byte("feeds")) 263 | if FeedsBucket == nil { 264 | return 265 | } 266 | FeedBucket := FeedsBucket.Bucket(p.Author) 267 | if FeedBucket == nil { 268 | return 269 | } 270 | LogBucket := FeedBucket.Bucket([]byte("log")) 271 | if LogBucket == nil { 272 | return 273 | } 274 | msgdata := LogBucket.Get(itob(p.Sequence)) 275 | if msgdata == nil { 276 | return 277 | } 278 | m = DecompressMessage(msgdata) 279 | return 280 | } 281 | 282 | var AddMessageHooks = map[string]func(m *SignedMessage, tx *bolt.Tx) error{} 283 | 284 | func (f *Feed) AddMessage(m *SignedMessage) error { 285 | if m != nil { 286 | f.addChan <- m 287 | } 288 | return nil 289 | } 290 | 291 | func (f *Feed) processMessageQueue() { 292 | for { 293 | f.waitingSignal.L.Lock() 294 | f.waitingSignal.Wait() 295 | f.waitingSignal.L.Unlock() 296 | newMsgs := []*SignedMessage{} 297 | err := f.store.db.Update(func(tx *bolt.Tx) error { 298 | f.waitingLock.Lock() 299 | f.SeqLock.Lock() 300 | defer func() { 301 | f.SeqLock.Unlock() 302 | f.waitingLock.Unlock() 303 | }() 304 | for { 305 | m, ok := f.waiting[f.LatestSeq+1] 306 | delete(f.waiting, f.LatestSeq+1) 307 | if !ok { 308 | break 309 | } 310 | 311 | if m.Author != f.ID { 312 | continue 313 | } 314 | if f.store.Get(nil, m.Key()) != nil { 315 | continue 316 | } 317 | err := m.Verify(tx, f) 318 | if err != nil { 319 | //fmt.Println(err) 320 | //fmt.Println((string(m.Message.Content))) 321 | fmt.Print("-") 322 | return err 323 | } 324 | err = f.addMessage(tx, m) 325 | if err != nil { 326 | fmt.Println("Bolt: ", err) 327 | return err 328 | } 329 | 330 | f.LatestSeq = m.Sequence 331 | fmt.Print("*") 332 | newMsgs = append(newMsgs, m) 333 | } 334 | return nil 335 | }) 336 | if err != nil { 337 | continue 338 | } 339 | for _, m := range newMsgs { 340 | f.Topic.Send <- m 341 | } 342 | } 343 | } 344 | 345 | func (f *Feed) addMessage(tx *bolt.Tx, m *SignedMessage) error { 346 | FeedsBucket, err := tx.CreateBucketIfNotExists([]byte("feeds")) 347 | if err != nil { 348 | return err 349 | } 350 | FeedBucket, err := FeedsBucket.CreateBucketIfNotExists(f.ID.DBKey()) 351 | if err != nil { 352 | return err 353 | } 354 | FeedLogBucket, err := FeedBucket.CreateBucketIfNotExists([]byte("log")) 355 | if err != nil { 356 | return err 357 | } 358 | FeedLogBucket.FillPercent = 1 359 | buf := m.Compress() 360 | err = FeedLogBucket.Put(itob(m.Sequence), buf) 361 | if err != nil { 362 | return err 363 | } 364 | LogBucket, err := tx.CreateBucketIfNotExists([]byte("log")) 365 | if err != nil { 366 | return err 367 | } 368 | LogBucket.FillPercent = 1 369 | seq, err := LogBucket.NextSequence() 370 | if err != nil { 371 | return err 372 | } 373 | err = LogBucket.Put(itob(int(seq)), m.Key().DBKey()) 374 | if err != nil { 375 | return err 376 | } 377 | PointerBucket, err := tx.CreateBucketIfNotExists([]byte("pointer")) 378 | if err != nil { 379 | return err 380 | } 381 | pointer := Pointer{Sequence: m.Sequence, LogKey: int(seq), Author: m.Author.DBKey()} 382 | buf = pointer.Marshal() 383 | err = PointerBucket.Put(m.Key().DBKey(), buf) 384 | if err != nil { 385 | return err 386 | } 387 | for module, hook := range AddMessageHooks { 388 | err = hook(m, tx) 389 | if err != nil { 390 | return fmt.Errorf("Bolt %s hook: %s", module, err) 391 | } 392 | } 393 | return nil 394 | } 395 | 396 | var RebuildClearHooks = map[string]func(tx *bolt.Tx) error{} 397 | 398 | func (ds *DataStore) RebuildAll() { 399 | log.Println("Starting rebuild of all indexes") 400 | count := 0 401 | ds.db.Update(func(tx *bolt.Tx) error { 402 | for module, hook := range RebuildClearHooks { 403 | err := hook(tx) 404 | if err != nil { 405 | return fmt.Errorf("Bolt %s hook: %s", module, err) 406 | } 407 | } 408 | 409 | LogBucket, err := tx.CreateBucketIfNotExists([]byte("log")) 410 | if err != nil { 411 | return err 412 | } 413 | cursor := LogBucket.Cursor() 414 | _, v := cursor.First() 415 | for v != nil { 416 | for module, hook := range AddMessageHooks { 417 | err = hook(ds.Get(tx, DBRef(v)), tx) 418 | if err != nil { 419 | return fmt.Errorf("Bolt %s hook: %s", module, err) 420 | } 421 | } 422 | count++ 423 | _, v = cursor.Next() 424 | } 425 | return nil 426 | }) 427 | log.Println("Finished rebuild of all modules") 428 | log.Println("Reindexed", count, "posts") 429 | } 430 | 431 | func (ds *DataStore) Rebuild(module string) { 432 | log.Println("Starting rebuild of", module) 433 | count := 0 434 | ds.db.Update(func(tx *bolt.Tx) error { 435 | if clear, ok := RebuildClearHooks[module]; ok { 436 | err := clear(tx) 437 | if err != nil { 438 | return err 439 | } 440 | } 441 | 442 | LogBucket, err := tx.CreateBucketIfNotExists([]byte("log")) 443 | if err != nil { 444 | return err 445 | } 446 | cursor := LogBucket.Cursor() 447 | _, v := cursor.First() 448 | for v != nil { 449 | AddMessageHooks[module](ds.Get(tx, DBRef(v)), tx) 450 | count++ 451 | _, v = cursor.Next() 452 | } 453 | return nil 454 | }) 455 | log.Println("Finished rebuild of", module) 456 | log.Println("Reindexed", count, "messages") 457 | } 458 | 459 | func (ds *DataStore) LatestCountFiltered(num int, start int, filter map[Ref]int) (msgs []*SignedMessage) { 460 | ds.db.View(func(tx *bolt.Tx) error { 461 | LogBucket := tx.Bucket([]byte("log")) 462 | if LogBucket == nil { 463 | return nil 464 | } 465 | cur := LogBucket.Cursor() 466 | _, val := cur.Last() 467 | for len(msgs) < num { 468 | for i := 0; i < start; i++ { 469 | _, val = cur.Prev() 470 | if val == nil { 471 | break 472 | } 473 | } 474 | if val == nil { 475 | break 476 | } 477 | msg := ds.Get(tx, DBRef(val)) 478 | 479 | if _, ok := filter[msg.Author]; ok && msg.Type() != "" { 480 | msgs = append(msgs, msg) 481 | } 482 | _, val = cur.Prev() 483 | } 484 | return nil 485 | }) 486 | return 487 | } 488 | 489 | func (f *Feed) PublishMessage(body interface{}) error { 490 | content, _ := Encode(body) 491 | 492 | m := &Message{ 493 | Author: f.ID, 494 | Timestamp: float64(time.Now().UnixNano() / int64(time.Millisecond)), 495 | Hash: "sha256", 496 | Content: content, 497 | Sequence: 1, 498 | } 499 | 500 | if l := f.Latest(); l != nil { 501 | key := l.Key() 502 | m.Previous = &key 503 | m.Sequence = l.Sequence + 1 504 | for m.Timestamp <= l.Timestamp { 505 | m.Timestamp += 0.01 506 | } 507 | } 508 | 509 | signer := f.store.Keys[f.ID] 510 | if signer == nil { 511 | return fmt.Errorf("Cannot sign message without signing key for feed") 512 | } 513 | sm := m.Sign(signer) 514 | c := f.Topic.Register(nil, true) 515 | err := f.AddMessage(sm) 516 | if err != nil { 517 | return err 518 | } 519 | 520 | for newm := range c { 521 | if newm.Key() == sm.Key() { 522 | f.Topic.Unregister(c) 523 | return nil 524 | } 525 | } 526 | 527 | return nil 528 | } 529 | 530 | func (f *Feed) Latest() (m *SignedMessage) { 531 | f.store.db.View(func(tx *bolt.Tx) error { 532 | FeedsBucket := tx.Bucket([]byte("feeds")) 533 | if FeedsBucket == nil { 534 | return nil 535 | } 536 | FeedBucket := FeedsBucket.Bucket(f.ID.DBKey()) 537 | if FeedBucket == nil { 538 | return nil 539 | } 540 | FeedLogBucket := FeedBucket.Bucket([]byte("log")) 541 | if FeedLogBucket == nil { 542 | return nil 543 | } 544 | cur := FeedLogBucket.Cursor() 545 | _, val := cur.Last() 546 | m = DecompressMessage(val) 547 | return nil 548 | }) 549 | return 550 | } 551 | 552 | func (f *Feed) LatestCount(num int, start int) (msgs []*SignedMessage) { 553 | f.store.db.View(func(tx *bolt.Tx) error { 554 | FeedsBucket := tx.Bucket([]byte("feeds")) 555 | if FeedsBucket == nil { 556 | return nil 557 | } 558 | FeedBucket := FeedsBucket.Bucket(f.ID.DBKey()) 559 | if FeedBucket == nil { 560 | return nil 561 | } 562 | FeedLogBucket := FeedBucket.Bucket([]byte("log")) 563 | if FeedLogBucket == nil { 564 | return nil 565 | } 566 | cur := FeedLogBucket.Cursor() 567 | _, val := cur.Last() 568 | for i := 0; i < start; i++ { 569 | _, val = cur.Prev() 570 | } 571 | for l1 := 0; l1 < num; l1++ { 572 | if val == nil { 573 | break 574 | } 575 | msg := DecompressMessage(val) 576 | if msg.Type() != "" { 577 | msgs = append(msgs, msg) 578 | } 579 | _, val = cur.Prev() 580 | } 581 | return nil 582 | }) 583 | return 584 | } 585 | 586 | func (f *Feed) GetSeq(tx *bolt.Tx, seq int) (m *SignedMessage) { 587 | if tx == nil { 588 | tx, _ = f.store.db.Begin(false) 589 | defer tx.Rollback() 590 | } 591 | FeedsBucket := tx.Bucket([]byte("feeds")) 592 | if FeedsBucket == nil { 593 | return nil 594 | } 595 | FeedBucket := FeedsBucket.Bucket(f.ID.DBKey()) 596 | if FeedBucket == nil { 597 | return nil 598 | } 599 | FeedLogBucket := FeedBucket.Bucket([]byte("log")) 600 | if FeedLogBucket == nil { 601 | return nil 602 | } 603 | val := FeedLogBucket.Get(itob(seq)) 604 | if val == nil { 605 | return nil 606 | } 607 | m = DecompressMessage(val) 608 | return m 609 | } 610 | 611 | var ErrLogClosed = errors.New("LogClosed") 612 | 613 | func (f *Feed) Log(seq int, live bool) chan *SignedMessage { 614 | c := make(chan *SignedMessage, 10) 615 | go func() { 616 | liveChan := make(chan *SignedMessage, 10) 617 | if live { 618 | f.Topic.Register(liveChan, false) 619 | } else { 620 | close(liveChan) 621 | } 622 | err := f.store.db.View(func(tx *bolt.Tx) error { 623 | FeedsBucket := tx.Bucket([]byte("feeds")) 624 | if FeedsBucket == nil { 625 | return nil 626 | } 627 | FeedBucket := FeedsBucket.Bucket(f.ID.DBKey()) 628 | if FeedBucket == nil { 629 | return nil 630 | } 631 | FeedLogBucket := FeedBucket.Bucket([]byte("log")) 632 | if FeedLogBucket == nil { 633 | return nil 634 | } 635 | err := FeedLogBucket.ForEach(func(k, v []byte) error { 636 | m := DecompressMessage(v) 637 | if m.Sequence < seq { 638 | return nil 639 | } 640 | seq = m.Sequence 641 | select { 642 | case c <- m: 643 | case <-time.After(1000 * time.Millisecond): 644 | close(c) 645 | return ErrLogClosed 646 | } 647 | return nil 648 | }) 649 | if err != nil { 650 | return err 651 | } 652 | return nil 653 | }) 654 | if err != nil { 655 | return 656 | } 657 | for m := range liveChan { 658 | if m.Sequence < seq { 659 | continue 660 | } 661 | seq = m.Sequence 662 | c <- m 663 | } 664 | close(c) 665 | }() 666 | return c 667 | } 668 | -------------------------------------------------------------------------------- /follower.go: -------------------------------------------------------------------------------- 1 | package ssb 2 | 3 | func (f *Feed) Follow(seq int, live bool, handler func(m *SignedMessage) error, done chan struct{}) error { 4 | for { 5 | f.SeqLock.Lock() 6 | if f.LatestSeq >= seq { 7 | f.SeqLock.Unlock() 8 | m := f.GetSeq(nil, seq) 9 | if m != nil { 10 | err := handler(m) 11 | if err != nil { 12 | return err 13 | } 14 | } 15 | seq++ 16 | } else { 17 | if !live { 18 | f.SeqLock.Unlock() 19 | return nil 20 | } 21 | c := f.Topic.Register(nil, false) 22 | f.SeqLock.Unlock() 23 | for { 24 | select { 25 | case m := <-c: 26 | err := handler(m) 27 | if err != nil { 28 | return nil 29 | } 30 | case <-done: 31 | f.Topic.Unregister(c) 32 | return nil 33 | } 34 | } 35 | 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "github.com/boltdb/bolt" 5 | 6 | "github.com/andyleap/go-ssb" 7 | "github.com/andyleap/go-ssb/blobs" 8 | ) 9 | 10 | type Repo struct { 11 | ds *ssb.DataStore 12 | Ref ssb.Ref 13 | } 14 | 15 | type RepoRoot struct { 16 | ssb.MessageBody 17 | Name string `json:"name"` 18 | } 19 | 20 | /* 21 | { 22 | type: 'git-update', 23 | repo: MsgId, 24 | repoBranch: [ MsgId ]?, 25 | refsBranch: [ MsgId ]?, 26 | refs: { : String|null }?, 27 | packs: [ BlobLink ]?, 28 | indexes: [ BlobLink ]?, 29 | head: string?, 30 | commits: [ { 31 | sha1: String, 32 | title: String, 33 | body: String?, 34 | parents: [ String ]?, 35 | } ]?, 36 | commits_more: Number?, 37 | num_objects: Number?, 38 | object_ids: [ String ]?, 39 | } 40 | */ 41 | type Commit struct { 42 | Sha1 string `json:"sha1"` 43 | Title string `json:"title"` 44 | Body string `json:"body,omitempty"` 45 | Parents []string `json:"parents,omitempty"` 46 | } 47 | 48 | type RepoUpdate struct { 49 | ssb.MessageBody 50 | Repo ssb.Ref `json:"repo"` 51 | RepoBranch []ssb.Ref `json:"repoBranch,omitempty"` 52 | RefsBranch []ssb.Ref `json:"refsBranch,omitempty"` 53 | Refs map[ssb.Ref]string `json:"refs,omitempty"` 54 | Packs []blobs.BlobLink `json:"packs,omitempty"` 55 | Indexes []blobs.BlobLink `json:"indexes,omitempty"` 56 | Head string `json:"Head,omitempty"` 57 | Commits []Commit `json:"commits,omitempty"` 58 | CommitsMore int `json:"commits_more,omitempty"` 59 | NumObjects int `json:"num_objects,omitempty"` 60 | ObjectIDs []string `json:"object_ids,omitempty"` 61 | } 62 | 63 | type RepoIssue struct { 64 | ssb.MessageBody 65 | Project ssb.Ref `json:"project"` 66 | Text string `json:"text"` 67 | } 68 | 69 | func init() { 70 | ssb.RebuildClearHooks["git"] = func(tx *bolt.Tx) error { 71 | tx.DeleteBucket([]byte("repos")) 72 | return nil 73 | } 74 | ssb.AddMessageHooks["git"] = func(m *ssb.SignedMessage, tx *bolt.Tx) error { 75 | _, mb := m.DecodeMessage() 76 | if _, ok := mb.(*RepoRoot); ok { 77 | ReposBucket, err := tx.CreateBucketIfNotExists([]byte("repos")) 78 | if err != nil { 79 | return err 80 | } 81 | repoBucket, err := ReposBucket.CreateBucketIfNotExists(m.Key().DBKey()) 82 | if err != nil { 83 | return err 84 | } 85 | err = repoBucket.Put([]byte("info"), m.Compress()) 86 | if err != nil { 87 | return err 88 | } 89 | return nil 90 | } 91 | if update, ok := mb.(*RepoUpdate); ok { 92 | ReposBucket, err := tx.CreateBucketIfNotExists([]byte("repos")) 93 | if err != nil { 94 | return err 95 | } 96 | repoBucket, err := ReposBucket.CreateBucketIfNotExists(update.Repo.DBKey()) 97 | if err != nil { 98 | return err 99 | } 100 | updateBucket, err := repoBucket.CreateBucketIfNotExists([]byte("updates")) 101 | if err != nil { 102 | return err 103 | } 104 | err = updateBucket.Put(m.Key().DBKey(), []byte{}) 105 | if err != nil { 106 | return err 107 | } 108 | blobBucket, err := repoBucket.CreateBucketIfNotExists([]byte("blobs")) 109 | if err != nil { 110 | return err 111 | } 112 | for _, pack := range update.Packs { 113 | err = blobBucket.Put(pack.Link.DBKey(), []byte{}) 114 | if err != nil { 115 | return err 116 | } 117 | } 118 | for _, index := range update.Indexes { 119 | err = blobBucket.Put(index.Link.DBKey(), []byte{}) 120 | if err != nil { 121 | return err 122 | } 123 | } 124 | } 125 | if issue, ok := mb.(*RepoIssue); ok { 126 | ReposBucket, err := tx.CreateBucketIfNotExists([]byte("repos")) 127 | if err != nil { 128 | return err 129 | } 130 | repoBucket, err := ReposBucket.CreateBucketIfNotExists(issue.Project.DBKey()) 131 | if err != nil { 132 | return err 133 | } 134 | issueBucket, err := repoBucket.CreateBucketIfNotExists([]byte("issues")) 135 | if err != nil { 136 | return err 137 | } 138 | err = issueBucket.Put(m.Key().DBKey(), []byte{}) 139 | if err != nil { 140 | return err 141 | } 142 | } 143 | 144 | return nil 145 | } 146 | ssb.MessageTypes["git-repo"] = func(mb ssb.MessageBody) interface{} { 147 | return &RepoRoot{MessageBody: mb} 148 | } 149 | ssb.MessageTypes["git-update"] = func(mb ssb.MessageBody) interface{} { 150 | return &RepoUpdate{MessageBody: mb} 151 | } 152 | ssb.MessageTypes["issue"] = func(mb ssb.MessageBody) interface{} { 153 | return &RepoIssue{MessageBody: mb} 154 | } 155 | } 156 | 157 | func Get(ds *ssb.DataStore, r ssb.Ref) *Repo { 158 | msg := ds.Get(nil, r) 159 | if msg == nil || msg.Type() != "git-repo" { 160 | return nil 161 | } 162 | return &Repo{ 163 | ds: ds, 164 | Ref: r, 165 | } 166 | } 167 | 168 | func (repo *Repo) WantAll() { 169 | repo.ds.DB().View(func(tx *bolt.Tx) error { 170 | 171 | ReposBucket := tx.Bucket([]byte("repos")) 172 | if ReposBucket == nil { 173 | return nil 174 | } 175 | repoBucket := ReposBucket.Bucket(repo.Ref.DBKey()) 176 | if repoBucket == nil { 177 | return nil 178 | } 179 | blobBucket := repoBucket.Bucket([]byte("blobs")) 180 | if blobBucket == nil { 181 | return nil 182 | } 183 | blobBucket.ForEach(func(k, v []byte) error { 184 | r := ssb.DBRef(k) 185 | blobs.Get(repo.ds).Want(r) 186 | 187 | return nil 188 | }) 189 | return nil 190 | }) 191 | } 192 | 193 | func (repo *Repo) ListBlobs() (b []ssb.Ref) { 194 | repo.ds.DB().View(func(tx *bolt.Tx) error { 195 | 196 | ReposBucket := tx.Bucket([]byte("repos")) 197 | if ReposBucket == nil { 198 | return nil 199 | } 200 | repoBucket := ReposBucket.Bucket(repo.Ref.DBKey()) 201 | if repoBucket == nil { 202 | return nil 203 | } 204 | blobBucket := repoBucket.Bucket([]byte("blobs")) 205 | if blobBucket == nil { 206 | return nil 207 | } 208 | blobBucket.ForEach(func(k, v []byte) error { 209 | r := ssb.DBRef(k) 210 | b = append(b, r) 211 | return nil 212 | }) 213 | return nil 214 | }) 215 | return 216 | } 217 | 218 | func (repo *Repo) ListUpdates() (b []ssb.Ref) { 219 | repo.ds.DB().View(func(tx *bolt.Tx) error { 220 | 221 | ReposBucket := tx.Bucket([]byte("repos")) 222 | if ReposBucket == nil { 223 | return nil 224 | } 225 | repoBucket := ReposBucket.Bucket(repo.Ref.DBKey()) 226 | if repoBucket == nil { 227 | return nil 228 | } 229 | blobBucket := repoBucket.Bucket([]byte("updates")) 230 | if blobBucket == nil { 231 | return nil 232 | } 233 | blobBucket.ForEach(func(k, v []byte) error { 234 | r := ssb.DBRef(k) 235 | b = append(b, r) 236 | return nil 237 | }) 238 | return nil 239 | }) 240 | return 241 | } 242 | 243 | func (repo *Repo) Issues() (issues []*ssb.SignedMessage) { 244 | repo.ds.DB().View(func(tx *bolt.Tx) error { 245 | 246 | ReposBucket := tx.Bucket([]byte("repos")) 247 | if ReposBucket == nil { 248 | return nil 249 | } 250 | repoBucket := ReposBucket.Bucket(repo.Ref.DBKey()) 251 | if repoBucket == nil { 252 | return nil 253 | } 254 | blobBucket := repoBucket.Bucket([]byte("issues")) 255 | if blobBucket == nil { 256 | return nil 257 | } 258 | blobBucket.ForEach(func(k, v []byte) error { 259 | r := ssb.DBRef(k) 260 | issues = append(issues, repo.ds.Get(tx, r)) 261 | return nil 262 | }) 263 | return nil 264 | }) 265 | return 266 | } 267 | -------------------------------------------------------------------------------- /go-ssb.go: -------------------------------------------------------------------------------- 1 | // go-ssb project go-ssb.go 2 | package ssb 3 | -------------------------------------------------------------------------------- /gossip/gossip.go: -------------------------------------------------------------------------------- 1 | package gossip 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "time" 10 | 11 | "github.com/boltdb/bolt" 12 | 13 | "github.com/andyleap/go-ssb" 14 | "github.com/andyleap/go-ssb/graph" 15 | "github.com/andyleap/go-ssb/muxrpcManager" 16 | "github.com/andyleap/muxrpc" 17 | "github.com/andyleap/muxrpc/codec" 18 | 19 | "cryptoscope.co/go/secretstream" 20 | "cryptoscope.co/go/secretstream/secrethandshake" 21 | ) 22 | 23 | type Pub struct { 24 | Link ssb.Ref `json:"key"` 25 | Host string `json:"host"` 26 | Port int `json:"port"` 27 | } 28 | 29 | type PubAnnounce struct { 30 | ssb.MessageBody 31 | Pub Pub `json:"address"` 32 | } 33 | 34 | func AddPub(ds *ssb.DataStore, pb Pub) { 35 | ds.DB().Update(func(tx *bolt.Tx) error { 36 | PubBucket, err := tx.CreateBucketIfNotExists([]byte("pubs")) 37 | if err != nil { 38 | return err 39 | } 40 | buf, _ := json.Marshal(pb) 41 | PubBucket.Put(pb.Link.DBKey(), buf) 42 | return nil 43 | }) 44 | } 45 | 46 | func AcceptInvite(ds *ssb.DataStore, pb Pub, invite []byte) error { 47 | if len(invite) != 32 { 48 | return fmt.Errorf("Invite seed length wrong, got %d, expecting 32", len(invite)) 49 | } 50 | keypair, err := secrethandshake.GenEdKeyPair(bytes.NewReader(invite)) 51 | fmt.Println(base64.StdEncoding.EncodeToString(keypair.Public[:]), ":", base64.StdEncoding.EncodeToString(keypair.Secret[:])) 52 | if err != nil { 53 | return err 54 | } 55 | c, err := secretstream.NewClient(*keypair, sbotAppKey) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | var pubKey [32]byte 61 | rawpubKey := pb.Link.Raw() 62 | copy(pubKey[:], rawpubKey) 63 | 64 | d, err := c.NewDialer(pubKey) 65 | if err != nil { 66 | return err 67 | } 68 | conn, err := d("tcp", fmt.Sprintf("%s:%d", pb.Host, pb.Port)) 69 | if err != nil { 70 | return err 71 | } 72 | muxconn := muxrpc.New(conn, nil) 73 | go muxconn.Handle() 74 | useReq := struct { 75 | Feed ssb.Ref `json:"feed"` 76 | }{ 77 | ds.PrimaryRef, 78 | } 79 | err = muxconn.Call("invite.use", nil, useReq) 80 | if err != nil { 81 | return err 82 | } 83 | AddPub(ds, pb) 84 | return nil 85 | } 86 | 87 | var sbotAppKey []byte 88 | 89 | func init() { 90 | sbotAppKey, _ = base64.StdEncoding.DecodeString("1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=") 91 | ssb.RebuildClearHooks["gossip"] = func(tx *bolt.Tx) error { 92 | tx.DeleteBucket([]byte("pubs")) 93 | return nil 94 | } 95 | ssb.AddMessageHooks["gossip"] = func(m *ssb.SignedMessage, tx *bolt.Tx) error { 96 | _, mb := m.DecodeMessage() 97 | if mbp, ok := mb.(*PubAnnounce); ok { 98 | if mbp.Pub.Link.Type != ssb.RefFeed { 99 | return nil 100 | } 101 | PubBucket, err := tx.CreateBucketIfNotExists([]byte("pubs")) 102 | if err != nil { 103 | return err 104 | } 105 | buf, _ := json.Marshal(mbp.Pub) 106 | err = PubBucket.Put(mbp.Pub.Link.DBKey(), buf) 107 | if err != nil { 108 | return err 109 | } 110 | return nil 111 | } 112 | return nil 113 | } 114 | ssb.MessageTypes["pub"] = func(mb ssb.MessageBody) interface{} { 115 | return &PubAnnounce{MessageBody: mb} 116 | } 117 | 118 | ssb.RegisterInit(func(ds *ssb.DataStore) { 119 | handlers, ok := ds.ExtraData("muxrpcHandlers").(map[string]func(conn *muxrpc.Conn, req int32, args json.RawMessage)) 120 | if !ok { 121 | handlers = map[string]func(conn *muxrpc.Conn, req int32, args json.RawMessage){} 122 | ds.SetExtraData("muxrpcHandlers", handlers) 123 | } 124 | handlers["createHistoryStream"] = func(conn *muxrpc.Conn, req int32, rm json.RawMessage) { 125 | params := struct { 126 | Id ssb.Ref `json:"id"` 127 | Seq int `json:"seq"` 128 | Live bool `json:"live"` 129 | }{ 130 | ssb.Ref{}, 131 | 0, 132 | false, 133 | } 134 | args := []interface{}{¶ms} 135 | json.Unmarshal(rm, &args) 136 | f := ds.GetFeed(params.Id) 137 | go func() { 138 | err := f.Follow(params.Seq, params.Live, func(m *ssb.SignedMessage) error { 139 | err := conn.Send(&codec.Packet{ 140 | Req: -req, 141 | Type: codec.JSON, 142 | Body: m.Encode(), 143 | Stream: true, 144 | }) 145 | return err 146 | }, conn.Done) 147 | if err != nil { 148 | log.Println(err) 149 | return 150 | } 151 | conn.Send(&codec.Packet{ 152 | Req: -req, 153 | Type: codec.JSON, 154 | Body: []byte("true"), 155 | Stream: true, 156 | EndErr: true, 157 | }) 158 | }() 159 | } 160 | onConnects, ok := ds.ExtraData("muxrpcOnConnect").(map[string]func(conn *muxrpc.Conn)) 161 | if !ok { 162 | onConnects = map[string]func(conn *muxrpc.Conn){} 163 | ds.SetExtraData("muxrpcOnConnect", onConnects) 164 | } 165 | onConnects["replicate"] = func(conn *muxrpc.Conn) { 166 | i := 0 167 | for feed := range graph.GetFollows(ds, ds.PrimaryRef, 2) { 168 | go func(feed ssb.Ref, i int) { 169 | time.Sleep(time.Duration(i) * 1 * time.Millisecond) 170 | f := ds.GetFeed(feed) 171 | if f == nil { 172 | return 173 | } 174 | seq := 0 175 | if f.Latest() != nil { 176 | seq = f.Latest().Sequence + 1 177 | } 178 | go func() { 179 | reply := func(p *codec.Packet) { 180 | if p.Type != codec.JSON { 181 | fmt.Println(p, string(p.Body)) 182 | return 183 | } 184 | var m *ssb.SignedMessage 185 | err := json.Unmarshal(p.Body, &m) 186 | if err != nil { 187 | fmt.Println(err, p, string(p.Body)) 188 | return 189 | } 190 | f.AddMessage(m) 191 | } 192 | err := conn.Source("createHistoryStream", reply, map[string]interface{}{"id": f.ID, "seq": seq, "live": true, "keys": false}) 193 | if err != nil { 194 | log.Println(err) 195 | } 196 | }() 197 | }(feed, i) 198 | i++ 199 | } 200 | } 201 | }) 202 | 203 | } 204 | 205 | func Replicate(ds *ssb.DataStore) { 206 | go func() { 207 | 208 | sss, _ := secretstream.NewServer(*ds.PrimaryKey, sbotAppKey) 209 | l, err := sss.Listen("tcp", ":8008") 210 | if err != nil { 211 | fmt.Println(err) 212 | return 213 | } 214 | for { 215 | conn, err := l.Accept() 216 | if err != nil { 217 | fmt.Println(err) 218 | return 219 | } 220 | remPubKey := conn.RemoteAddr().(secretstream.Addr).PubKey() 221 | remRef, _ := ssb.NewRef(ssb.RefFeed, remPubKey, ssb.RefAlgoEd25519) 222 | go muxrpcManager.HandleConn(ds, remRef, conn) 223 | } 224 | }() 225 | go func() { 226 | ed := ds.ExtraData("muxrpcConns").(*muxrpcManager.ExtraData) 227 | ssc, _ := secretstream.NewClient(*ds.PrimaryKey, sbotAppKey) 228 | pubList := GetPubs(ds) 229 | t := time.NewTicker(5 * time.Second) 230 | for range t.C { 231 | fmt.Println("tick") 232 | ed.Lock.Lock() 233 | connCount := len(ed.Conns) 234 | ed.Lock.Unlock() 235 | if connCount >= 3 { 236 | continue 237 | } 238 | if len(pubList) == 0 { 239 | pubList = GetPubs(ds) 240 | } 241 | if len(pubList) == 0 { 242 | continue 243 | } 244 | pub := pubList[0] 245 | pubList = pubList[1:] 246 | 247 | ed.Lock.Lock() 248 | _, ok := ed.Conns[pub.Link] 249 | ed.Lock.Unlock() 250 | if ok { 251 | continue 252 | } 253 | 254 | var pubKey [32]byte 255 | rawpubKey := pub.Link.Raw() 256 | copy(pubKey[:], rawpubKey) 257 | 258 | d, err := ssc.NewDialer(pubKey) 259 | if err != nil { 260 | continue 261 | } 262 | go func() { 263 | log.Println("Connecting to ", pub) 264 | conn, err := d("tcp", fmt.Sprintf("%s:%d", pub.Host, pub.Port)) 265 | if err != nil { 266 | log.Println(err) 267 | return 268 | } 269 | end := time.NewTimer(5 * time.Minute) 270 | go func() { 271 | for range end.C { 272 | conn.Close() 273 | } 274 | }() 275 | muxrpcManager.HandleConn(ds, pub.Link, conn) 276 | end.Stop() 277 | }() 278 | 279 | } 280 | }() 281 | } 282 | 283 | func GetPubs(ds *ssb.DataStore) (pds []*Pub) { 284 | ds.DB().View(func(tx *bolt.Tx) error { 285 | PubBucket := tx.Bucket([]byte("pubs")) 286 | if PubBucket == nil { 287 | return nil 288 | } 289 | PubBucket.ForEach(func(k, v []byte) error { 290 | var pd *Pub 291 | json.Unmarshal(v, &pd) 292 | pds = append(pds, pd) 293 | return nil 294 | }) 295 | return nil 296 | }) 297 | return 298 | } 299 | 300 | /*func HandleConn(ds *ssb.DataStore, muxConn *muxrpc.Client) { 301 | muxConn.HandleSource("createHistoryStream", func(rm json.RawMessage) chan interface{} { 302 | params := struct { 303 | Id ssb.Ref `json:"id"` 304 | Seq int `json:"seq"` 305 | Live bool `json:"live"` 306 | }{ 307 | ssb.Ref{}, 308 | 0, 309 | false, 310 | } 311 | args := []interface{}{¶ms} 312 | json.Unmarshal(rm, &args) 313 | f := ds.GetFeed(params.Id) 314 | if f.ID == ds.PrimaryRef { 315 | fmt.Println(params) 316 | fmt.Println(string(rm)) 317 | } 318 | c := make(chan interface{}) 319 | go func() { 320 | for m := range f.Log(params.Seq, params.Live) { 321 | fmt.Println("Sending", m.Author, m.Sequence) 322 | c <- m 323 | } 324 | close(c) 325 | }() 326 | return c 327 | }) 328 | 329 | go func() { 330 | i := 0 331 | for feed := range graph.GetFollows(ds, ds.PrimaryRef, 2) { 332 | go func(feed ssb.Ref, i int) { 333 | time.Sleep(time.Duration(i) * 50 * time.Millisecond) 334 | reply := make(chan *ssb.SignedMessage) 335 | f := ds.GetFeed(feed) 336 | if f == nil { 337 | return 338 | } 339 | seq := 0 340 | if f.Latest() != nil { 341 | seq = f.Latest().Sequence + 1 342 | } 343 | go func() { 344 | muxConn.Source("createHistoryStream", reply, map[string]interface{}{"id": f.ID, "seq": seq, "live": true, "keys": false}) 345 | close(reply) 346 | }() 347 | for m := range reply { 348 | if m.Sequence == 0 { 349 | continue 350 | } 351 | fmt.Print("*") 352 | f.AddMessage(m) 353 | } 354 | }(feed, i) 355 | i++ 356 | } 357 | }() 358 | muxConn.Handle() 359 | } 360 | */ 361 | -------------------------------------------------------------------------------- /graph/graph.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/andyleap/go-ssb" 7 | "github.com/boltdb/bolt" 8 | ) 9 | 10 | type Relation struct { 11 | Following bool 12 | Blocking bool 13 | } 14 | 15 | type Contact struct { 16 | ssb.MessageBody 17 | Contact ssb.Ref `json:"contact"` 18 | Following *bool `json:"following,omitempty"` 19 | Blocking *bool `json:"blocking,omitempty"` 20 | } 21 | 22 | func init() { 23 | ssb.RebuildClearHooks["graph"] = func(tx *bolt.Tx) error { 24 | tx.DeleteBucket([]byte("graph")) 25 | return nil 26 | } 27 | ssb.AddMessageHooks["graph"] = handleGraph 28 | ssb.MessageTypes["contact"] = func(mb ssb.MessageBody) interface{} { return &Contact{MessageBody: mb} } 29 | } 30 | 31 | func handleGraph(m *ssb.SignedMessage, tx *bolt.Tx) error { 32 | _, mb := m.DecodeMessage() 33 | if mbc, ok := mb.(*Contact); ok { 34 | GraphBucket, err := tx.CreateBucketIfNotExists([]byte("graph")) 35 | if err != nil { 36 | return err 37 | } 38 | if mbc.Contact.Type != ssb.RefFeed { 39 | return nil 40 | } 41 | FeedBucket, err := GraphBucket.CreateBucketIfNotExists(m.Author.DBKey()) 42 | var r Relation 43 | json.Unmarshal(FeedBucket.Get(mbc.Contact.DBKey()), &r) 44 | if err != nil { 45 | return err 46 | } 47 | if mbc.Following != nil { 48 | r.Following = *mbc.Following 49 | } 50 | if mbc.Blocking != nil { 51 | r.Blocking = *mbc.Blocking 52 | } 53 | buf, _ := json.Marshal(r) 54 | err = FeedBucket.Put(mbc.Contact.DBKey(), buf) 55 | if err != nil { 56 | return err 57 | } 58 | } 59 | return nil 60 | } 61 | 62 | func GetFollows(ds *ssb.DataStore, feed ssb.Ref, depth int) (follows map[ssb.Ref]int) { 63 | follows = map[ssb.Ref]int{} 64 | follows[feed] = 0 65 | ds.DB().View(func(tx *bolt.Tx) error { 66 | GraphBucket := tx.Bucket([]byte("graph")) 67 | if GraphBucket == nil { 68 | return nil 69 | } 70 | for l1 := 0; l1 < depth; l1++ { 71 | for k, v := range follows { 72 | if v == l1 { 73 | FeedBucket := GraphBucket.Bucket(k.DBKey()) 74 | if FeedBucket == nil { 75 | continue 76 | } 77 | FeedBucket.ForEach(func(k, v []byte) error { 78 | if len(k) == 0 { 79 | return nil 80 | } 81 | if _, ok := follows[ssb.DBRef(k)]; !ok { 82 | var r Relation 83 | json.Unmarshal(v, &r) 84 | if r.Following { 85 | follows[ssb.DBRef(k)] = l1 + 1 86 | } 87 | } 88 | return nil 89 | }) 90 | } 91 | } 92 | } 93 | return nil 94 | }) 95 | return 96 | } 97 | -------------------------------------------------------------------------------- /keys.go: -------------------------------------------------------------------------------- 1 | package ssb 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "golang.org/x/crypto/ed25519" 7 | ) 8 | 9 | type Signer interface { 10 | Sign([]byte) Signature 11 | } 12 | 13 | type SignerEd25519 struct { 14 | Private ed25519.PrivateKey 15 | } 16 | 17 | func (k SignerEd25519) Sign(content []byte) Signature { 18 | return Signature(base64.StdEncoding.EncodeToString(ed25519.Sign(k.Private, content)) + ".sig.ed25519") 19 | } 20 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package ssb 2 | 3 | import ( 4 | "bytes" 5 | "compress/flate" 6 | "crypto/sha256" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "strings" 11 | 12 | "github.com/boltdb/bolt" 13 | ) 14 | 15 | type SignedMessage struct { 16 | Message 17 | Signature Signature `json:"signature"` 18 | } 19 | 20 | type Message struct { 21 | Previous *Ref `json:"previous"` 22 | Author Ref `json:"author"` 23 | Sequence int `json:"sequence"` 24 | Timestamp float64 `json:"timestamp"` 25 | Hash string `json:"hash"` 26 | Content json.RawMessage `json:"content"` 27 | } 28 | 29 | func Encode(i interface{}) ([]byte, error) { 30 | var buf bytes.Buffer 31 | enc := json.NewEncoder(&buf) 32 | enc.SetEscapeHTML(false) 33 | enc.SetIndent("", " ") 34 | err := enc.Encode(i) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return bytes.Trim(buf.Bytes(), "\n"), nil 39 | } 40 | 41 | func (m *SignedMessage) Verify(tx *bolt.Tx, f *Feed) error { 42 | buf, err := Encode(m.Message) 43 | if err != nil { 44 | return err 45 | } 46 | err = m.Signature.Verify(buf, m.Author) 47 | if err != nil { 48 | return err 49 | } 50 | if m.Sequence == 1 { 51 | return nil 52 | } 53 | latest := f.GetSeq(tx, m.Sequence-1) 54 | if latest == nil && m.Previous != nil { 55 | fmt.Println(string(m.Encode())) 56 | return fmt.Errorf("Expected message") 57 | } 58 | if m.Previous == nil && latest == nil { 59 | return nil 60 | } 61 | if m.Previous == nil && latest != nil { 62 | fmt.Println(string(m.Encode())) 63 | return fmt.Errorf("Error: expected previous %s but found %s", latest.Key(), "") 64 | } 65 | if latest != nil && m.Sequence == latest.Sequence { 66 | return fmt.Errorf("Error: Repeated message") 67 | } 68 | if *m.Previous != latest.Key() { 69 | /*buf, _ := Encode(latest) 70 | buf2 := ToJSBinary(buf) 71 | 72 | buf3, _ := Encode(m) 73 | fmt.Printf("\n%q\n%q\n%q\n", string(buf), string(buf2), string(buf3))*/ 74 | return fmt.Errorf("Error: expected previous %s but found %s", latest.Key(), *m.Previous) 75 | } 76 | if m.Sequence != latest.Sequence+1 || m.Timestamp <= latest.Timestamp { 77 | return fmt.Errorf("Error: out of order") 78 | } 79 | return nil 80 | } 81 | 82 | func (m *SignedMessage) Encode() []byte { 83 | buf, _ := Encode(m) 84 | return buf 85 | } 86 | 87 | func (m *SignedMessage) Compress() []byte { 88 | buf := m.Encode() 89 | cbuf := bytes.Buffer{} 90 | cbuf.WriteByte(2) 91 | cwrite, _ := flate.NewWriterDict(&cbuf, 9, Compression2) 92 | cwrite.Write(buf) 93 | cwrite.Flush() 94 | cwrite.Close() 95 | return cbuf.Bytes() 96 | } 97 | 98 | func DecompressMessage(cbuf []byte) *SignedMessage { 99 | switch cbuf[0] { 100 | case 1: 101 | reader := flate.NewReaderDict(bytes.NewReader(cbuf[1:]), Compression1) 102 | buf, _ := ioutil.ReadAll(reader) 103 | reader.Close() 104 | var m *SignedMessage 105 | json.Unmarshal(buf, &m) 106 | return m 107 | case 2: 108 | reader := flate.NewReaderDict(bytes.NewReader(cbuf[1:]), Compression2) 109 | buf, _ := ioutil.ReadAll(reader) 110 | reader.Close() 111 | var m *SignedMessage 112 | json.Unmarshal(buf, &m) 113 | return m 114 | default: 115 | var m *SignedMessage 116 | json.Unmarshal(cbuf, &m) 117 | return m 118 | } 119 | 120 | } 121 | 122 | func (m *SignedMessage) Key() Ref { 123 | if m == nil { 124 | return Ref{} 125 | } 126 | buf, _ := Encode(m) 127 | /*enc := RemoveUnsupported(charmap.ISO8859_1.NewEncoder()) 128 | buf, err := enc.Bytes(buf) 129 | if err != nil { 130 | panic(err) 131 | }*/ 132 | buf = ToJSBinary(buf) 133 | switch strings.ToLower(m.Hash) { 134 | case "sha256": 135 | hash := sha256.Sum256(buf) 136 | ref, _ := NewRef(RefMessage, hash[:], RefAlgoSha256) 137 | return ref 138 | } 139 | fmt.Println(string(buf)) 140 | return Ref{} 141 | } 142 | 143 | func (m *Message) Sign(s Signer) *SignedMessage { 144 | content, _ := Encode(m) 145 | sig := s.Sign(content) 146 | return &SignedMessage{Message: *m, Signature: sig} 147 | } 148 | -------------------------------------------------------------------------------- /messagetopic.go: -------------------------------------------------------------------------------- 1 | package ssb 2 | 3 | import "sync" 4 | 5 | type MessageTopic struct { 6 | lock sync.Mutex 7 | recps map[chan *SignedMessage]bool 8 | Send chan *SignedMessage 9 | } 10 | 11 | func NewMessageTopic() *MessageTopic { 12 | mt := &MessageTopic{Send: make(chan *SignedMessage, 10), recps: map[chan *SignedMessage]bool{}} 13 | go mt.process() 14 | return mt 15 | } 16 | 17 | func (mt *MessageTopic) Close() { 18 | close(mt.Send) 19 | } 20 | 21 | func (mt *MessageTopic) process() { 22 | for m := range mt.Send { 23 | func() { 24 | mt.lock.Lock() 25 | defer mt.lock.Unlock() 26 | for recp, strict := range mt.recps { 27 | if strict { 28 | recp <- m 29 | } else { 30 | select { 31 | case recp <- m: 32 | default: 33 | delete(mt.recps, recp) 34 | close(recp) 35 | } 36 | } 37 | } 38 | }() 39 | 40 | } 41 | mt.lock.Lock() 42 | defer mt.lock.Unlock() 43 | for recp := range mt.recps { 44 | delete(mt.recps, recp) 45 | close(recp) 46 | } 47 | } 48 | 49 | func (mt *MessageTopic) Register(recp chan *SignedMessage, strict bool) chan *SignedMessage { 50 | mt.lock.Lock() 51 | defer mt.lock.Unlock() 52 | if recp == nil { 53 | recp = make(chan *SignedMessage, 1) 54 | } 55 | mt.recps[recp] = strict 56 | return recp 57 | } 58 | 59 | func (mt *MessageTopic) Unregister(recp chan *SignedMessage) { 60 | mt.lock.Lock() 61 | defer mt.lock.Unlock() 62 | 63 | if _, ok := mt.recps[recp]; ok { 64 | delete(mt.recps, recp) 65 | close(recp) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /messagetypes.go: -------------------------------------------------------------------------------- 1 | package ssb 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type MessageBody struct { 8 | Type string `json:"type"` 9 | Message *Message `json:"-"` 10 | } 11 | 12 | var MessageTypes = map[string]func(mb MessageBody) interface{}{} 13 | 14 | func (m *Message) DecodeMessage() (t string, mb interface{}) { 15 | Type := &MessageBody{} 16 | json.Unmarshal(m.Content, &Type) 17 | Type.Message = m 18 | if mf, ok := MessageTypes[Type.Type]; ok { 19 | mb = mf(*Type) 20 | } 21 | t = Type.Type 22 | json.Unmarshal(m.Content, &mb) 23 | return 24 | } 25 | 26 | func (m *Message) Type() string { 27 | Type := &MessageBody{} 28 | json.Unmarshal(m.Content, &Type) 29 | return Type.Type 30 | } 31 | -------------------------------------------------------------------------------- /muxrpcManager/muxrpcManager.go: -------------------------------------------------------------------------------- 1 | package muxrpcManager 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "sync" 7 | 8 | "github.com/andyleap/go-ssb" 9 | "github.com/andyleap/muxrpc" 10 | ) 11 | 12 | type ExtraData struct { 13 | Lock sync.Mutex 14 | Conns map[ssb.Ref]*muxrpc.Conn 15 | } 16 | 17 | func init() { 18 | ssb.RegisterInit(func(ds *ssb.DataStore) { 19 | ed := &ExtraData{Conns: map[ssb.Ref]*muxrpc.Conn{}} 20 | ds.SetExtraData("muxrpcConns", ed) 21 | }) 22 | } 23 | 24 | func HandleConn(ds *ssb.DataStore, ref ssb.Ref, conn io.ReadWriteCloser) { 25 | ed := ds.ExtraData("muxrpcConns").(*ExtraData) 26 | 27 | handlers := ds.ExtraData("muxrpcHandlers").(map[string]func(conn *muxrpc.Conn, req int32, args json.RawMessage)) 28 | 29 | muxConn := muxrpc.New(conn, handlers) 30 | 31 | ed.Lock.Lock() 32 | ed.Conns[ref] = muxConn 33 | ed.Lock.Unlock() 34 | 35 | onConnect, onConnectOK := ds.ExtraData("muxrpcOnConnect").(map[string]func(conn *muxrpc.Conn)) 36 | 37 | if onConnectOK { 38 | for _, oc := range onConnect { 39 | go oc(muxConn) 40 | } 41 | } 42 | 43 | muxConn.Handle() 44 | ed.Lock.Lock() 45 | delete(ed.Conns, ref) 46 | ed.Lock.Unlock() 47 | } 48 | -------------------------------------------------------------------------------- /ref.go: -------------------------------------------------------------------------------- 1 | package ssb 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "errors" 8 | "strings" 9 | 10 | "golang.org/x/crypto/ed25519" 11 | ) 12 | 13 | type Ref struct { 14 | Type RefType 15 | Data string 16 | Algo RefAlgo 17 | } 18 | 19 | type RefType int 20 | 21 | func (rt RefType) String() string { 22 | switch rt { 23 | case RefFeed: 24 | return "@" 25 | case RefMessage: 26 | return "%" 27 | case RefBlob: 28 | return "&" 29 | default: 30 | return "?" 31 | } 32 | } 33 | 34 | const ( 35 | RefInvalid RefType = iota 36 | RefFeed 37 | RefMessage 38 | RefBlob 39 | ) 40 | 41 | type RefAlgo int 42 | 43 | func (ra RefAlgo) String() string { 44 | switch ra { 45 | case RefAlgoSha256: 46 | return "sha256" 47 | case RefAlgoEd25519: 48 | return "ed25519" 49 | default: 50 | return "???" 51 | } 52 | } 53 | 54 | const ( 55 | RefAlgoInvalid RefAlgo = iota 56 | RefAlgoSha256 57 | RefAlgoEd25519 58 | ) 59 | 60 | var ( 61 | ErrInvalidRefType = errors.New("Invalid Ref Type") 62 | ErrInvalidRefAlgo = errors.New("Invalid Ref Algo") 63 | ErrInvalidSig = errors.New("Invalid Signature") 64 | ErrInvalidHash = errors.New("Invalid Hash") 65 | ) 66 | 67 | func NewRef(typ RefType, raw []byte, algo RefAlgo) (Ref, error) { 68 | return Ref{typ, string(raw), algo}, nil 69 | } 70 | 71 | func (r Ref) Raw() []byte { 72 | return []byte(r.Data) 73 | } 74 | 75 | func (r Ref) DBKey() []byte { 76 | return append([]byte{byte(r.Type), byte(r.Algo)}, []byte(r.Data)...) 77 | } 78 | 79 | func DBRef(ref []byte) Ref { 80 | return Ref{Type: RefType(ref[0]), Data: string(ref[2:]), Algo: RefAlgo(ref[1])} 81 | } 82 | 83 | func ParseRef(ref string) Ref { 84 | parts := strings.Split(strings.Trim(ref, "@%&"), ".") 85 | if len(parts) != 2 { 86 | return Ref{} 87 | } 88 | r := Ref{} 89 | switch ref[0] { 90 | case '@': 91 | r.Type = RefFeed 92 | case '%': 93 | r.Type = RefMessage 94 | case '&': 95 | r.Type = RefBlob 96 | default: 97 | return Ref{} 98 | } 99 | switch strings.ToLower(parts[1]) { 100 | case "sha256": 101 | r.Algo = RefAlgoSha256 102 | case "ed25519": 103 | r.Algo = RefAlgoEd25519 104 | default: 105 | return Ref{} 106 | } 107 | buf, _ := base64.StdEncoding.DecodeString(parts[0]) 108 | r.Data = string(buf) 109 | return r 110 | } 111 | 112 | func (r Ref) String() string { 113 | if r.Type == RefInvalid || r.Algo == RefAlgoInvalid { 114 | return "" 115 | } 116 | return r.Type.String() + base64.StdEncoding.EncodeToString([]byte(r.Data)) + "." + r.Algo.String() 117 | } 118 | 119 | func (r Ref) MarshalText() (text []byte, err error) { 120 | return []byte(r.String()), nil 121 | } 122 | 123 | func (r *Ref) UnmarshalText(text []byte) error { 124 | *r = ParseRef(string(text)) 125 | return nil 126 | } 127 | 128 | func (r Ref) IsMessage() bool { 129 | return r.Type == RefMessage 130 | } 131 | 132 | func (r Ref) CheckHash(content []byte) error { 133 | switch r.Algo { 134 | case RefAlgoSha256: 135 | contentHash := sha256.Sum256(content) 136 | if bytes.Equal(r.Raw(), contentHash[:]) { 137 | return nil 138 | } 139 | return ErrInvalidHash 140 | } 141 | return ErrInvalidHash 142 | } 143 | 144 | type Signature string 145 | 146 | type SigAlgo int 147 | 148 | const ( 149 | SigAlgoInvalid SigAlgo = iota 150 | SigAlgoEd25519 151 | ) 152 | 153 | func (s Signature) Algo() SigAlgo { 154 | parts := strings.Split(string(s), ".") 155 | if len(parts) != 3 || parts[1] != "sig" { 156 | return SigAlgoInvalid 157 | } 158 | switch strings.ToLower(parts[2]) { 159 | case "ed25519": 160 | return SigAlgoEd25519 161 | } 162 | return SigAlgoInvalid 163 | } 164 | 165 | func (s Signature) Raw() []byte { 166 | b64 := strings.Split(string(s), ".")[0] 167 | raw, err := base64.StdEncoding.DecodeString(b64) 168 | if err != nil { 169 | return nil 170 | } 171 | 172 | return raw 173 | } 174 | 175 | func (s Signature) Verify(content []byte, r Ref) error { 176 | switch s.Algo() { 177 | case SigAlgoEd25519: 178 | if r.Algo != RefAlgoEd25519 { 179 | return ErrInvalidSig 180 | } 181 | rawkey := r.Raw() 182 | if rawkey == nil { 183 | return nil 184 | } 185 | 186 | key := ed25519.PublicKey(rawkey) 187 | if ed25519.Verify(key, content, s.Raw()) { 188 | return nil 189 | } 190 | return ErrInvalidSig 191 | } 192 | return ErrInvalidSig 193 | } 194 | -------------------------------------------------------------------------------- /rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net" 8 | "reflect" 9 | 10 | "github.com/andyleap/go-ssb" 11 | ) 12 | 13 | type Request struct { 14 | Method string `json:"method"` 15 | Params json.RawMessage `json:"params"` 16 | ID interface{} `json:"id"` 17 | } 18 | 19 | type Response struct { 20 | Result interface{} `json:"result"` 21 | Error interface{} `json:"error"` 22 | ID interface{} `json:"id"` 23 | } 24 | 25 | func ServeConn(datastore *ssb.DataStore, conn io.ReadWriteCloser) { 26 | reader := json.NewDecoder(conn) 27 | writer := json.NewEncoder(conn) 28 | resp := make(chan interface{}) 29 | defer conn.Close() 30 | go func() { 31 | for v := range resp { 32 | writer.Encode(v) 33 | } 34 | }() 35 | for { 36 | var req Request 37 | err := reader.Decode(&req) 38 | if err != nil { 39 | return 40 | } 41 | RPCMethods, ok := datastore.ExtraData("RPCMethods").(map[string]interface{}) 42 | if !ok { 43 | if req.ID != nil { 44 | resp <- Response{Result: nil, Error: "No such method", ID: req.ID} 45 | } 46 | continue 47 | } 48 | method, ok := RPCMethods[req.Method] 49 | if !ok { 50 | if req.ID != nil { 51 | resp <- Response{Result: nil, Error: "No such method", ID: req.ID} 52 | } 53 | continue 54 | } 55 | go func() { 56 | defer func() { 57 | if r := recover(); r != nil { 58 | if req.ID != nil { 59 | resp <- Response{Result: nil, Error: fmt.Sprintf("Panic while running method: %s", r), ID: req.ID} 60 | } 61 | } 62 | }() 63 | rval := reflect.ValueOf(method) 64 | if rval.Kind() != reflect.Func { 65 | if req.ID != nil { 66 | resp <- Response{Result: nil, Error: "No such method", ID: req.ID} 67 | } 68 | return 69 | } 70 | params := []reflect.Value{} 71 | decodeparams := []interface{}{} 72 | rtype := rval.Type() 73 | for l1 := 0; l1 < rtype.NumIn(); l1++ { 74 | pval := reflect.New(rtype.In(l1)) 75 | params = append(params, pval.Elem()) 76 | decodeparams = append(decodeparams, pval.Interface()) 77 | } 78 | err := json.Unmarshal(req.Params, &decodeparams) 79 | if err != nil { 80 | if req.ID != nil { 81 | resp <- Response{Result: nil, Error: fmt.Sprintf("Error decoding method parameters: %s", err), ID: req.ID} 82 | } 83 | } 84 | ret := rval.Call(params) 85 | if req.ID != nil { 86 | if rtype.NumOut() == 2 { 87 | if ret[1].Interface() != nil { 88 | resp <- Response{Result: nil, Error: ret[1].Interface().(error).Error(), ID: req.ID} 89 | } else { 90 | resp <- Response{Result: ret[0], Error: nil, ID: req.ID} 91 | } 92 | } else { 93 | if rtype.Out(0).Implements(reflect.TypeOf((*error)(nil)).Elem()) { 94 | if ret[0].Interface() != nil { 95 | resp <- Response{Result: nil, Error: ret[0].Interface().(error).Error(), ID: req.ID} 96 | } else { 97 | resp <- Response{Result: true, Error: nil, ID: req.ID} 98 | } 99 | } else { 100 | resp <- Response{Result: ret[0], Error: nil, ID: req.ID} 101 | } 102 | } 103 | } 104 | }() 105 | } 106 | } 107 | 108 | func ListenAndServe(datastore *ssb.DataStore, n string, a string) error { 109 | l, err := net.Listen(n, a) 110 | defer l.Close() 111 | if err != nil { 112 | return err 113 | } 114 | for { 115 | c, err := l.Accept() 116 | if err != nil { 117 | return err 118 | } 119 | go func() { 120 | ServeConn(datastore, c) 121 | }() 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /search/search.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/andyleap/go-ssb" 7 | "github.com/andyleap/go-ssb/social" 8 | "github.com/boltdb/bolt" 9 | ) 10 | 11 | func Search(ds *ssb.DataStore, term string, max int) (found []*ssb.SignedMessage) { 12 | ds.DB().View(func(tx *bolt.Tx) error { 13 | 14 | LogBucket := tx.Bucket([]byte("log")) 15 | if LogBucket == nil { 16 | return nil 17 | } 18 | cursor := LogBucket.Cursor() 19 | _, v := cursor.Last() 20 | for v != nil { 21 | m := ds.Get(tx, ssb.DBRef(v)) 22 | _, md := m.DecodeMessage() 23 | if post, ok := md.(*social.Post); ok { 24 | if strings.Contains(post.Text, term) { 25 | found = append(found, m) 26 | if max > 0 && len(found) >= max { 27 | return nil 28 | } 29 | } 30 | } 31 | _, v = cursor.Prev() 32 | } 33 | return nil 34 | }) 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /social/social.go: -------------------------------------------------------------------------------- 1 | package social 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/andyleap/go-ssb" 9 | "github.com/boltdb/bolt" 10 | ) 11 | 12 | func itob(v int) []byte { 13 | b := make([]byte, 8) 14 | binary.BigEndian.PutUint64(b, uint64(v)) 15 | return b 16 | } 17 | 18 | type Link struct { 19 | Link ssb.Ref `json:"link"` 20 | } 21 | 22 | type Image struct { 23 | image 24 | } 25 | 26 | type image struct { 27 | Link ssb.Ref `json:"link"` 28 | Width int `json:"width,omitempty"` 29 | Height int `json:"height,omitempty"` 30 | Name string `json:"name,omitempty"` 31 | Size int `json:"size,omitempty"` 32 | Type string `json:"type,omitempty"` 33 | } 34 | 35 | func (i *Image) UnmarshalJSON(b []byte) error { 36 | err := json.Unmarshal(b, &i.image) 37 | if err != nil { 38 | err = json.Unmarshal(b, &i.Link) 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | type Post struct { 47 | ssb.MessageBody 48 | Text string `json:"text"` 49 | Channel string `json:"channel,omitempty"` 50 | Root ssb.Ref `json:"root,omitempty"` 51 | Branch ssb.Ref `json:"branch,omitempty"` 52 | Recps []Link `json:"recps,omitempty"` 53 | Mentions []Link `json:"mentions,omitempty"` 54 | } 55 | 56 | type About struct { 57 | ssb.MessageBody 58 | About ssb.Ref `json:"about"` 59 | Name string `json:"name,omitempty"` 60 | Image *Image `json:"image,omitempty"` 61 | } 62 | 63 | type Vote struct { 64 | ssb.MessageBody 65 | Vote struct { 66 | Link ssb.Ref `json:"link"` 67 | Value int `json:"value"` 68 | Reason string `json:"reason,omitempty"` 69 | } `json:"vote"` 70 | } 71 | 72 | func init() { 73 | ssb.MessageTypes["post"] = func(mb ssb.MessageBody) interface{} { return &Post{MessageBody: mb} } 74 | ssb.MessageTypes["about"] = func(mb ssb.MessageBody) interface{} { return &About{MessageBody: mb} } 75 | ssb.MessageTypes["vote"] = func(mb ssb.MessageBody) interface{} { return &Vote{MessageBody: mb} } 76 | ssb.RebuildClearHooks["social"] = func(tx *bolt.Tx) error { 77 | tx.DeleteBucket([]byte("votes")) 78 | tx.DeleteBucket([]byte("threads")) 79 | b, _ := tx.CreateBucketIfNotExists([]byte("feeds")) 80 | b.ForEach(func(k, v []byte) error { 81 | b.Bucket(k).Delete([]byte("about")) 82 | return nil 83 | }) 84 | 85 | return nil 86 | } 87 | ssb.AddMessageHooks["social"] = func(m *ssb.SignedMessage, tx *bolt.Tx) error { 88 | _, mb := m.DecodeMessage() 89 | if mba, ok := mb.(*About); ok { 90 | if mba.About == m.Author { 91 | FeedsBucket, err := tx.CreateBucketIfNotExists([]byte("feeds")) 92 | if err != nil { 93 | return err 94 | } 95 | FeedBucket, err := FeedsBucket.CreateBucketIfNotExists(m.Author.DBKey()) 96 | if err != nil { 97 | return err 98 | } 99 | aboutdata := FeedBucket.Get([]byte("about")) 100 | var a About 101 | if aboutdata != nil { 102 | json.Unmarshal(aboutdata, &a) 103 | } 104 | if mba.Name != "" { 105 | a.Name = mba.Name 106 | } 107 | if mba.Image != nil { 108 | a.Image = mba.Image 109 | } 110 | buf, err := json.Marshal(a) 111 | if err != nil { 112 | return err 113 | } 114 | err = FeedBucket.Put([]byte("about"), buf) 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | } 120 | if vote, ok := mb.(*Vote); ok { 121 | VotesBucket, err := tx.CreateBucketIfNotExists([]byte("votes")) 122 | if err != nil { 123 | return err 124 | } 125 | votesRaw := VotesBucket.Get(vote.Vote.Link.DBKey()) 126 | var votes []ssb.Ref 127 | if votesRaw != nil { 128 | json.Unmarshal(votesRaw, &votes) 129 | } 130 | votes = append(votes, m.Key()) 131 | buf, _ := json.Marshal(votes) 132 | 133 | err = VotesBucket.Put(vote.Vote.Link.DBKey(), buf) 134 | if err != nil { 135 | return err 136 | } 137 | } 138 | if post, ok := mb.(*Post); ok { 139 | if post.Root.Type != ssb.RefInvalid { 140 | ThreadsBucket, err := tx.CreateBucketIfNotExists([]byte("threads")) 141 | if err != nil { 142 | return err 143 | } 144 | ThreadBucket, err := ThreadsBucket.CreateBucketIfNotExists(post.Root.DBKey()) 145 | if err != nil { 146 | return err 147 | } 148 | logBucket, err := ThreadBucket.CreateBucketIfNotExists([]byte("log")) 149 | if err != nil { 150 | return err 151 | } 152 | logBucket.FillPercent = 1 153 | seq, err := logBucket.NextSequence() 154 | if err != nil { 155 | return err 156 | } 157 | logBucket.Put(itob(int(seq)), m.Key().DBKey()) 158 | 159 | timeBucket, err := ThreadBucket.CreateBucketIfNotExists([]byte("time")) 160 | if err != nil { 161 | return err 162 | } 163 | i := int(m.Timestamp * float64(time.Millisecond)) 164 | for timeBucket.Get(itob(i)) != nil { 165 | i++ 166 | } 167 | timeBucket.Put(itob(i), m.Key().DBKey()) 168 | } 169 | } 170 | return nil 171 | } 172 | } 173 | 174 | func GetAbout(tx *bolt.Tx, ref ssb.Ref) (a *About) { 175 | FeedsBucket := tx.Bucket([]byte("feeds")) 176 | if FeedsBucket == nil { 177 | return 178 | } 179 | FeedBucket := FeedsBucket.Bucket(ref.DBKey()) 180 | if FeedBucket == nil { 181 | return 182 | } 183 | aboutdata := FeedBucket.Get([]byte("about")) 184 | if aboutdata == nil { 185 | return 186 | } 187 | json.Unmarshal(aboutdata, &a) 188 | return 189 | } 190 | 191 | func GetVotes(tx *bolt.Tx, ref ssb.Ref) []*ssb.SignedMessage { 192 | VotesBucket := tx.Bucket([]byte("votes")) 193 | if VotesBucket == nil { 194 | return nil 195 | } 196 | votesRaw := VotesBucket.Get(ref.DBKey()) 197 | var voteRefs []ssb.Ref 198 | if votesRaw != nil { 199 | json.Unmarshal(votesRaw, &voteRefs) 200 | } 201 | votes := make([]*ssb.SignedMessage, 0, len(voteRefs)) 202 | for _, r := range voteRefs { 203 | msg := ssb.GetMsg(tx, r) 204 | if msg == nil { 205 | continue 206 | } 207 | votes = append(votes, msg) 208 | } 209 | return votes 210 | } 211 | 212 | func GetThread(tx *bolt.Tx, ref ssb.Ref) []*ssb.SignedMessage { 213 | ThreadsBucket := tx.Bucket([]byte("threads")) 214 | if ThreadsBucket == nil { 215 | return nil 216 | } 217 | ThreadBucket := ThreadsBucket.Bucket(ref.DBKey()) 218 | if ThreadBucket == nil { 219 | return nil 220 | } 221 | timeBucket := ThreadBucket.Bucket([]byte("time")) 222 | if timeBucket == nil { 223 | return nil 224 | } 225 | thread := []*ssb.SignedMessage{} 226 | timeBucket.ForEach(func(k, v []byte) error { 227 | msg := ssb.GetMsg(tx, ssb.DBRef(v)) 228 | if msg != nil { 229 | thread = append(thread, msg) 230 | } 231 | return nil 232 | }) 233 | return thread 234 | } 235 | --------------------------------------------------------------------------------