fix up tests, add docs

This commit is contained in:
tsmethurst 2021-09-08 12:25:14 +02:00 committed by tsmethurst
commit e2d97c1ce7
33 changed files with 497 additions and 170 deletions

View file

@ -20,7 +20,7 @@ package main
import ( import (
"github.com/superseriousbusiness/gotosocial/internal/cliactions/admin/account" "github.com/superseriousbusiness/gotosocial/internal/cliactions/admin/account"
"github.com/superseriousbusiness/gotosocial/internal/cliactions/admin/export" "github.com/superseriousbusiness/gotosocial/internal/cliactions/admin/trans"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -161,7 +161,21 @@ func adminCommands() []*cli.Command {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return runAction(c, export.Export) return runAction(c, trans.Export)
},
},
{
Name: "import",
Usage: "import data from a file into the database",
Flags: []cli.Flag{
&cli.StringFlag{
Name: config.TransPathFlag,
Usage: config.TransPathUsage,
Required: true,
},
},
Action: func(c *cli.Context) error {
return runAction(c, trans.Import)
}, },
}, },
}, },

View file

@ -213,3 +213,78 @@ Example:
```bash ```bash
gotosocial admin account password --username some_username --pasword some_really_good_password gotosocial admin account password --username some_username --pasword some_really_good_password
``` ```
### gotosocial admin export
This command can be used to export data from your GoToSocial instance into a file, for backup/storage.
The file format will be a series of newline-separated JSON objects.
`gotosocial admin export --help`:
```text
NAME:
gotosocial admin export - export data from the database to file at the given path
USAGE:
gotosocial admin export [command options] [arguments...]
OPTIONS:
--path value the path of the file to import from/export to
--help, -h show help (default: false)
```
Example:
```bash
gotosocial admin export --path ./example.json
```
`example.json`:
```json
{"type":"account","id":"01F8MH5NBDF2MV7CTC4Q5128HF","createdAt":"2021-08-31T12:00:53.985645Z","username":"1happyturtle","locked":true,"language":"en","uri":"http://localhost:8080/users/1happyturtle","url":"http://localhost:8080/@1happyturtle","inboxURI":"http://localhost:8080/users/1happyturtle/inbox","outboxURI":"http://localhost:8080/users/1happyturtle/outbox","followingUri":"http://localhost:8080/users/1happyturtle/following","followersUri":"http://localhost:8080/users/1happyturtle/followers","featuredCollectionUri":"http://localhost:8080/users/1happyturtle/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAzLP7oyyR+BU9ejn0CN9K+WpX3L37pxUcCgZAGH5lf3cGPZjz\nausfsFME94OjVyzw3K5M2beDkZ4E+Fak46NLtakLB1yovy9jKtj4Y4txHoMvRJLz\neUPxdfeXtpx2d3FDj++Uq4DEE0BhbePXhTGJWaNdC9MQmWKghJnCS5mrnFkdpEFx\njUz9l0UHl2Z4wppxPdpt7FyevcdfKqzGsAA3BxTM0dg47ZJWjtcvfCiSYpAKFNJY\nfKhKn9T3ezZgrLsF+o0IpD23KxWe1X4d5lgJRU9T4FmLmbvyJKUnfgYXbSEvLUcq\n79WbhgRcWwxWubjmWXgPGwzULVhhpYlwd2Cv3wIDAQABAoIBAGF+MxHjD15VV2NY\nKKb1GjMx98i1Xx6TijgoA+zmfha4LGu35e79Lql+0LXFp0zEpa6lAQsMQQhgd0OD\nmKKmSk+pxAvskJ4FxrhIf/yBFA4RMrj5OCaAOocRtdsOJ8n5UtFBrNAF0tzMY9q/\nkgzoq97aVF1mV9iFxaeBx6zT8ozSdqBq1PK/3w1dVg89S5tfKYc7Q0lQ00SfsTnd\niTDClKyqurebo9Pt6M7gXavgg3tvBlmwwr6XHs34Leng3oiN9mW8DVzaBMPzn+rE\nxF2eqs3v9vVpj8es88OwCh5P+ff8vJYvhu7Fcr/bJ8BItBQwfb8QBDATg/MXU2BI\n2ssW6AECgYEA4wmIyYGeu9+hzDa/J3Vh8GnlVNUCohHcChQdOsWsFXUgpVlUIHrX\neKHn42vD4Rzy52/YzJts4NkZTM9sL+kEXIEcpMG/S9xIIud7U0m/hMSAlmnJK/9j\niEXws3o4jo0E77jnRcBdIjpG4K5Eekm0DSR3SFhtZfEdN2DWPvu7K98CgYEA5tER\n/qJwFMc51AobMU87ZjXON7hI2U1WY/pVF62jSl0IcSsnj2riEKWLrs+GRG+HUg+U\naFSqAHcxaVHA0h0AYR8RopAhDdVKh0kvB8biLo+IEzNjPv2vyn0yRN5YSfXdGzyJ\nUjVU6kWdQOwmzy86nHgFaqEx7eofHIaGZzJK/AECgYEAu2VNQHX63TuzQuoVUa5z\nzoq5vhGsALYZF0CO98ndRkDNV22qIL0ESQ/qZS64GYFZhWouWoQXlGfdmCbFN65v\n6SKwz9UT3rvN1vGWO6Ltr9q6AG0EnYpJT1vbV2kUcaU4Y94NFue2d9/+TMnKv91B\n/m8Q/efvNGuWH/WQIaCKV6UCgYBz89WhYMMDfS4M2mLcu5vwddk53qciGxrqMMjs\nkzsz0Va7W12NS7lzeWaZlAE0gf6t98urOdUJVNeKvBoss4sMP0phqxwf0eWV3ur0\ncjIQB+TpGGikLVdRVuGY/UXHKe9AjoHBva8B3aTpB3lbnbNJBXZbIc1uYq3sa5w7\nXWWUAQKBgH3yW73RRpQNcc9hTUssomUsnQQgHxpfWx5tNxqod36Ytd9EKBh3NqUZ\nvPcH6gdh7mcnNaVNTtQOHLHsbPfBK/pqvb3MAsdlokJcQz8MQJ9SGBBPY6PaGw8z\nq/ambaQykER6dwlXTIlU20uXY0bttOL/iYjKmgo3vA66qfzS6nsg\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAzLP7oyyR+BU9ejn0CN9K+WpX3L37pxUcCgZAGH5lf3cGPZjzausf\nsFME94OjVyzw3K5M2beDkZ4E+Fak46NLtakLB1yovy9jKtj4Y4txHoMvRJLzeUPx\ndfeXtpx2d3FDj++Uq4DEE0BhbePXhTGJWaNdC9MQmWKghJnCS5mrnFkdpEFxjUz9\nl0UHl2Z4wppxPdpt7FyevcdfKqzGsAA3BxTM0dg47ZJWjtcvfCiSYpAKFNJYfKhK\nn9T3ezZgrLsF+o0IpD23KxWe1X4d5lgJRU9T4FmLmbvyJKUnfgYXbSEvLUcq79Wb\nhgRcWwxWubjmWXgPGwzULVhhpYlwd2Cv3wIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/1happyturtle#main-key"}
{"type":"account","id":"01F8MH0BBE4FHXPH513MBVFHB0","createdAt":"2021-09-08T10:00:53.985634Z","username":"weed_lord420","locked":true,"language":"en","uri":"http://localhost:8080/users/weed_lord420","url":"http://localhost:8080/@weed_lord420","inboxURI":"http://localhost:8080/users/weed_lord420/inbox","outboxURI":"http://localhost:8080/users/weed_lord420/outbox","followingUri":"http://localhost:8080/users/weed_lord420/following","followersUri":"http://localhost:8080/users/weed_lord420/followers","featuredCollectionUri":"http://localhost:8080/users/weed_lord420/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAzsCcTHzwIgpWKVvut0Q/t1bFwnbj9hO6Ic6k0KXCXbf6qi0b\nMIyLRZr8DS61mD+SPSO2QKEL647xxyW2D8YGtwN6Cc6MpWETsWJkNtS8t7tDL//P\nceYpo5LiqKgn0TXj0Pq8Lvb7rqpH8QJ2EVm14SK+elhKZW/Bi5ZOEwfL8pw6EHI4\nus6VxCNQ099dksu++kbdD7zxqEKnk/4zOttYt0whlVrxzkibTjlKdlSlTYpIstU+\nfNyYVE0xWvrn+yF7jVlEwZYOFGfZbpELadrdOr2k1hvAk7upkrpKmLqYfwqD/xPc\nqwtx0iS6AEnmkSiTcAvju5vLkoLFRU7Of4AZ2wIDAQABAoIBAEAA4GHNS4k+Ke4j\nx4J0XkUjV5UbuPY0pSpSDjOJHOJmUfLcg85Ds9mYYO6zxwOaqmrC42ieclI5rh84\nTWQUqX9+VAk1J9UKeE4xZ1SSBtnZ3rK9PjrERZ+dmQ0dATaCuEO5Wwgu7Trk++Bg\nIqy8WNGZL94v9tfwALp1jTXW9AvmQoNdCFBP62vcmYW4YLjnggxLCFTA8YKfdePa\nTuxxY6uLkeBbxzWpbRU2+bmlxd5OnCkiRSMHIX+6JdtCu2JdWpUTCnWrFi2n1TZz\nZQx9z5rvowK1O785jGMFum5vBWpjIU8sJcXmPjGMU25zzmrhzfmkJsTXER3CXoUo\nSqSPqgECgYEA78OR7bY5KKQQ7Lyz6dru4Fct5P/OXTQoOg5aS7TKb95LVWj+TANn\n5djwIbLmAUV30z0Id9VgiZOL0Hny8+3VV9eU088Z408pAy5WQrL3dB8tZLUJSq5c\n5k6X15/VjWOOZKppDxShzoV3mcohrnwVwkv4fhPFQQOJJBYz6xurWs0CgYEA3MDE\nsDMd9ahzO0dl62ynojkkA8ZTcn2UdyvLpGj9UxT5j9vWF3CfqitXgcpNiVSIbxqQ\nbo/pBch7c/2Xakv5zkdcrJj5/6gyr+m1/tK2o7+CjDaSE4SYwufXx+qkl03Zpyzt\nKdOi7Hz/b2tdjump7ECEDE45mG2ea8oSnPgXl0cCgYBkGGFzu/9g2B24t47ksmHH\nhp3CXIjqoDurARLxSCi7SzJoFc0ULtfRPSAC8YzUOwwrQ++lF4+V3+MexcqHy2Kl\nqXqYcn18SC/3BAE/Fzf3Yoyw3mNiqihefbEmc7PTsxxfKkVx5ksmzNGBgsFM9sCe\nvNigyeAvpCo8xogmPwbqgQKBgE34mIBTzcUzFmBdu5YH7r3RyPK8XkUWLhZZlbgg\njTmHMw6o61mkIgENBf+F4RUckoQLsfAbTIcKZPB3JcAZzcYaVpVwAv1V/3E671lu\nO6xivE2iCL50GzDcis7GBhSbHsF5kNsxMV6uV9qW5ZjQ13/m2b0u9BDuxwHzgdeH\nmW2JAoGAIUOYniuEwdygxWVnYatpr3NPjT3BOKoV5i9zkeJRu1hFpwQM6vQ4Ds5p\nGC5vbMKAv9Cwuw62e2HvqTun3+U2Y5Uived3XCpgM/50BFrFHCfuqXEnu1bEzk5z\n9mIhp8uXPxzC5N7tRQfb3/eU1IUcb6T6ksbr2P81z0j03J55erg=\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAzsCcTHzwIgpWKVvut0Q/t1bFwnbj9hO6Ic6k0KXCXbf6qi0bMIyL\nRZr8DS61mD+SPSO2QKEL647xxyW2D8YGtwN6Cc6MpWETsWJkNtS8t7tDL//PceYp\no5LiqKgn0TXj0Pq8Lvb7rqpH8QJ2EVm14SK+elhKZW/Bi5ZOEwfL8pw6EHI4us6V\nxCNQ099dksu++kbdD7zxqEKnk/4zOttYt0whlVrxzkibTjlKdlSlTYpIstU+fNyY\nVE0xWvrn+yF7jVlEwZYOFGfZbpELadrdOr2k1hvAk7upkrpKmLqYfwqD/xPcqwtx\n0iS6AEnmkSiTcAvju5vLkoLFRU7Of4AZ2wIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/weed_lord420#main-key"}
{"type":"account","id":"01F8MH17FWEB39HZJ76B6VXSKF","createdAt":"2021-09-05T10:00:53.985641Z","username":"admin","locked":true,"language":"en","uri":"http://localhost:8080/users/admin","url":"http://localhost:8080/@admin","inboxURI":"http://localhost:8080/users/admin/inbox","outboxURI":"http://localhost:8080/users/admin/outbox","followingUri":"http://localhost:8080/users/admin/following","followersUri":"http://localhost:8080/users/admin/followers","featuredCollectionUri":"http://localhost:8080/users/admin/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxr2e1pqfLwwUCwHUdx56Mxnq5Kzc2EBwqN6jIPjiqVaG5eVq\nhujDhdqwMq0hnpBSPzLnvjiOtEh7Bwhx0MjuC/GRPTM9oNWPYD4PcjX5ofrubyLR\nBI97qD0SbyzUWzeyBi6R5tpW8LK1MJXNbnYlz5WouEiC4mY77ulri0EN2hCq80wg\nfvtEjEvELcKBqIytKH3rutIzfAyqXD7LSQ8UDoNh9GHyIfq8Zj32gWVk2MiPI3+G\n8kQJDmD8CKEasnrGVdSJBQUg3xDAtOibPXLP+07AIsKYMon35hVNvQNQPS7ru/Bk\nRhhGp2R44zqj6L9mxYbSrhFAaKDedu8oVe1aLQIDAQABAoIBAGK0aIADOU4ffJDe\n7sveiih5Fc1PATwx/QIR2QkWM1SREdx6LYclcX44V8xDanAbE44p1SkHY/CsEtYy\nXnyoXnn2FwFDQrdveY7+I6PApOPLAcKWkyLltC+hbVdj92/6YGNrm7EA/a77wruH\nmwjiivLnTG2CLecNiXSl33DA9YU4Yz+2Tza3IpTdjt8c/dz/BKKaxaWV+i9ew5VR\nioo5v51B+J8PrneCM/p8LGiLV148Njr0JqV6eFy1JuzItYMYdc3Fp+YnMzsuMZEA\n1akMcoln/ucVJyOFnCn6jx47nIoPZLl1KxX3aRDRfvrejm6W4yAkkTmR5voSRqax\njPL3rI0CgYEA9Acu4TO8xJ3uGaUad0N9JTYQVSmtAaE/g+df9LGMSzoj8X95S4xE\nQsGPqNGDm2VWADJjK4P05twZ+LfsfSKQ86wbp4/gbgnXpqB1P5Lty/B7KxiTnNwt\nwb1WGWTCukxfUSL3PRyf8uylkrg72RxKiBx4zKO3WVSLWOZWrFtn0qMCgYEA0H2p\nJs9Nv20ADOOX5tQ7+ruS6/B/Fhyj5fhflSYCAtOW7aME7+zQKJyqSQZ4b2Aub3Tp\nGIaUbRIGzjHyuTultFFWvjU3H5aI/0g1G9WKaBhNkyTIYVmMKtYyhXNvouWing8x\noraWx8TTBP8Cdnnk+QgdR2fpug8cghKupp5wvO8CgYA1JFtRL7MsHjh73TimQExA\njkWARlMmx7bNQtXis8eZmk+5h8kiaqly4DQoz3eZn7fa0x5Fm7b5j3UYdPVLSvvG\nFPTwyKRXUk1kPA1MivK+NuCbwf5jao+MYW8emJLPf1JCmRq+dD1g6aglC3n9Dewt\nOAYWipCjI4Y1FfRKFJ3HgQKBgEAb47+DTyzln3ZXJYZdDHR06SCTuwBZnixAy2NZ\nZJTp6yb3UbVU5E0Yn2QFEVNuB9lN4b8g4tMHEACnazN6G+HugPXL9z9HUqjs0yfT\n6dNIZdIxJUyJ9IfXhYFzlYhJhE+F7IVUD9kttJV8tI0pvja1QAuM8Fm9+84jYIDr\nh08RAoGAMYbjKHbtejcHBwt1kIcSss0cDmlZbBleJo8tdmdg4ndf5GE9N4/EL7tq\nm2zYSfr7OVdnOwRhoO+xF/6d1L7+TR1wz+k2fuMsI71aM5Ocp1nYTutjIkBTcldZ\nZzvjOgZWng5icuRLQQiDSKG5uqazqL/xGXkijb4kp4WW6myWY3c=\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAxr2e1pqfLwwUCwHUdx56Mxnq5Kzc2EBwqN6jIPjiqVaG5eVqhujD\nhdqwMq0hnpBSPzLnvjiOtEh7Bwhx0MjuC/GRPTM9oNWPYD4PcjX5ofrubyLRBI97\nqD0SbyzUWzeyBi6R5tpW8LK1MJXNbnYlz5WouEiC4mY77ulri0EN2hCq80wgfvtE\njEvELcKBqIytKH3rutIzfAyqXD7LSQ8UDoNh9GHyIfq8Zj32gWVk2MiPI3+G8kQJ\nDmD8CKEasnrGVdSJBQUg3xDAtOibPXLP+07AIsKYMon35hVNvQNQPS7ru/BkRhhG\np2R44zqj6L9mxYbSrhFAaKDedu8oVe1aLQIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/admin#main-key"}
{"type":"account","id":"01F8MH1H7YV1Z7D2C8K2730QBF","createdAt":"2021-09-06T10:00:53.985643Z","username":"the_mighty_zork","locked":true,"language":"en","uri":"http://localhost:8080/users/the_mighty_zork","url":"http://localhost:8080/@the_mighty_zork","inboxURI":"http://localhost:8080/users/the_mighty_zork/inbox","outboxURI":"http://localhost:8080/users/the_mighty_zork/outbox","followingUri":"http://localhost:8080/users/the_mighty_zork/following","followersUri":"http://localhost:8080/users/the_mighty_zork/followers","featuredCollectionUri":"http://localhost:8080/users/the_mighty_zork/collections/featured","actorType":"Person","privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApBmF8U+or+E0mgUMH3LE4uRIWzeV9rhYnvSMm9OpOsxwJiss\n5mEA/NtPHvQlq2UwrqXX89Wvu94K9EzZ4VyWYQGdxaiPpt17vRqUfsHUnXkY0pvC\nC9zt/aNlJtdt2xm+7PTC0YQd4+E1FX3aaoUPJL8MXzNlpJzaUtuwLZe1iBmFfatZ\nFHptEgc4nlf6TNLTzj3Yw1/7zIGVS8Vi7VquHc0Xo8dRiL2RxCGzLWnwL6GlrxY1\ntMhsUg467XeoiwegFCpcIhAhPFREKoTnCEksL/N0rpXl7m6CAy5uqBGs5mMXnXlq\nefr58l0j2dU6zc60LCHH9TJC+roXsKJhy9sx/QIDAQABAoIBAFa+UypbFG1cW2Tr\nNBxPm7ngOEtXl8MicV4dIVKh0TwOo13ZxtNFBbOj7jALmPn/9HrtmbkABPQHDL1U\n/nt9aNSAeTjpwH3RaD5vFX3n0g8n2zJBOZLxxzAjNi4RBLYj5uP1AiKkdvRlsJza\nuSFDkty2zMBqN9mLPHE+RePj5Qa6tjYfIQqQzu/+YnYMlXHoC2yHNKsvz6S5FhVj\nv5zATv2JlJQH3RSmhuPOah73iQnKCLzYYEAHleawKrCg/rZ3ht37Guvabeq7MqQN\nvi9pJdAA+RMxPsboHajskePjOTYJgKQSxEAMRTMfBR40aZxklxQL0EoBd1Y3CHXh\nfMg0xWECgYEA0ORrpJ1A2WNQwKcDDeBBsaJqWF4EraoFzYrugKZrAYEeVyuGD0zq\nARUaWkZTZ1f6wQ10i1WxAuKlBEds7QsLdZzLsA4um4JlBroCZiYfPnmTtb8op1LY\nFqeYTByvAmnfWWTuOI67GX9ruLg8tEGuz38kuQVSxYs51its3tScNPUCgYEAyRst\nwRbqpOqnwoRoS6pxv0Vpc3nUcfaVYwsg/qobJkiwAdlUYeE7alvEY926VW4cvU/X\nhy3L1punAqnyLI7uuqCefXEbNxO0Cebyy4Kv2Ye1uzl0OHsJczSNdfpNqfAIKwtN\nHLCYDGCsluQhz+I/5Pd0dT+JDPPW9hKS2HG7o+kCgYBqugn1VRLo/sEnbS02TbnC\n1ESZWY/yWsgUOEObH2vUnO+vgeFAt/9nBi0sqnm6d0z6jbFZ7zI9UycUhJm2ksoM\nEUxQay6M7ZZIVYkcP6X++YbqePyAYOdey8oYOR+BkC45MkQ0SVh2so+LFTaOsnBq\nO3+7uGiN3ZBzSESbpO0acQKBgQCONrsXZeZO82XpB4tdns3LbgGRWKEkajTgEnml\nvZNvck2NMSwb/5PttbFe0ei4CyMluPV4MamJPQ9Qse+BFR67OWR63uZY/4T8z6X4\nxpUmZnLcUFfgrRlUr+AtgvEy8HxGPDquxC7x6deC6RcEFEIM3/UqCOEZGMJ1x1Ky\n31LLKQKBgGCKwVgQ8+4JyHZFkie3YdHhxJDokgY+Opb0HNnoBY/lZ54UMCCJQPS2\n0XPSu651j/3adr3RQneU04gF6U2/D5JzFEV0kUsqZ4Zy2EEU0LU4ibus0gyomSpK\niWhU4QrC/M4ELxYZinlNu3ThPWNQ/PMNteVWfdgOcV7uUWl0ViFp\n-----END RSA PRIVATE KEY-----\n","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEApBmF8U+or+E0mgUMH3LE4uRIWzeV9rhYnvSMm9OpOsxwJiss5mEA\n/NtPHvQlq2UwrqXX89Wvu94K9EzZ4VyWYQGdxaiPpt17vRqUfsHUnXkY0pvCC9zt\n/aNlJtdt2xm+7PTC0YQd4+E1FX3aaoUPJL8MXzNlpJzaUtuwLZe1iBmFfatZFHpt\nEgc4nlf6TNLTzj3Yw1/7zIGVS8Vi7VquHc0Xo8dRiL2RxCGzLWnwL6GlrxY1tMhs\nUg467XeoiwegFCpcIhAhPFREKoTnCEksL/N0rpXl7m6CAy5uqBGs5mMXnXlqefr5\n8l0j2dU6zc60LCHH9TJC+roXsKJhy9sx/QIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://localhost:8080/users/the_mighty_zork#main-key"}
{"type":"block","id":"01FEXXET6XXMF7G2V3ASZP3YQW","createdAt":"2021-09-08T09:00:53.965362Z","uri":"http://localhost:8080/users/1happyturtle/blocks/01FEXXET6XXMF7G2V3ASZP3YQW","accountId":"01F8MH5NBDF2MV7CTC4Q5128HF","targetAccountId":"01F8MH5ZK5VRH73AKHQM6Y9VNX"}
{"type":"account","id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","createdAt":"2021-08-31T12:00:53.985646Z","username":"foss_satan","domain":"fossbros-anonymous.io","locked":true,"language":"en","uri":"http://fossbros-anonymous.io/users/foss_satan","url":"http://fossbros-anonymous.io/@foss_satan","inboxURI":"http://fossbros-anonymous.io/users/foss_satan/inbox","outboxURI":"http://fossbros-anonymous.io/users/foss_satan/outbox","followingUri":"http://fossbros-anonymous.io/users/foss_satan/following","followersUri":"http://fossbros-anonymous.io/users/foss_satan/followers","featuredCollectionUri":"http://fossbros-anonymous.io/users/foss_satan/collections/featured","actorType":"Person","publicKey":"-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEA2OyVgkaIL9VohXKYTh319j4OouHRX/8QC7piXj71k7q5RDzEyvis\nVZBc5/C1/crCpxt895i0Ai2CiXQx+dISV7s/JBhAGl8s7TQ8jLlMuptrI0+sdkBC\nlu8pU0qQmoeXVnlquOzNmqGufUxIDtLXLZDN17qf/7vWA23q4d0tG5KQhGGGKiVM\n61Ufvr9MmgPBSpyUvYMAulFlz1264L49aGWeVgOz3qUQzqtxjrP0kaIbeyt56miP\nKr5AqkRgSsXci+FAo6suxR5gzo9NgleNkbZWF9MQyKlawukPwZUDSh396vtNQMee\n/4mto7mAXw8iio0IacrYO3F7iyewXnmI/QIDAQAB\n-----END RSA PUBLIC KEY-----\n","publicKeyUri":"http://fossbros-anonymous.io/users/foss_satan/main-key"}
{"type":"follow","id":"01F8PYDCE8XE23GRE5DPZJDZDP","createdAt":"2021-09-08T09:00:54.749465Z","uri":"http://localhost:8080/users/the_mighty_zork/follow/01F8PYDCE8XE23GRE5DPZJDZDP","accountId":"01F8MH1H7YV1Z7D2C8K2730QBF","targetAccountId":"01F8MH5NBDF2MV7CTC4Q5128HF"}
{"type":"follow","id":"01F8PY8RHWRQZV038T4E8T9YK8","createdAt":"2021-09-06T12:00:54.749459Z","uri":"http://localhost:8080/users/the_mighty_zork/follow/01F8PY8RHWRQZV038T4E8T9YK8","accountId":"01F8MH1H7YV1Z7D2C8K2730QBF","targetAccountId":"01F8MH17FWEB39HZJ76B6VXSKF"}
{"type":"domainBlock","id":"01FF22EQM7X8E3RX1XGPN7S87D","createdAt":"2021-09-08T10:00:53.968971Z","domain":"replyguys.com","createdByAccountID":"01F8MH17FWEB39HZJ76B6VXSKF","privateComment":"i blocked this domain because they keep replying with pushy + unwarranted linux advice","publicComment":"reply-guying to tech posts","obfuscate":false}
{"type":"user","id":"01F8MGYG9E893WRHW0TAEXR8GJ","createdAt":"2021-09-08T10:00:53.97247Z","accountID":"01F8MH0BBE4FHXPH513MBVFHB0","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","locale":"en","lastEmailedAt":"0001-01-01T00:00:00Z","confirmationToken":"a5a280bd-34be-44a3-8330-a57eaf61b8dd","confirmationTokenSentAt":"2021-09-08T10:00:53.972472Z","unconfirmedEmail":"weed_lord420@example.org","moderator":false,"admin":false,"disabled":false,"approved":false}
{"type":"user","id":"01F8MGWYWKVKS3VS8DV1AMYPGE","createdAt":"2021-09-05T10:00:53.972475Z","email":"admin@example.org","accountID":"01F8MH17FWEB39HZJ76B6VXSKF","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","currentSignInAt":"2021-09-08T09:50:53.972477Z","lastSignInAt":"2021-09-08T08:00:53.972477Z","chosenLanguages":["en"],"locale":"en","lastEmailedAt":"2021-09-08T09:30:53.972478Z","confirmedAt":"2021-09-05T10:00:53.972478Z","moderator":true,"admin":true,"disabled":false,"approved":true}
{"type":"user","id":"01F8MGVGPHQ2D3P3X0454H54Z5","createdAt":"2021-09-06T22:00:53.97248Z","email":"zork@example.org","accountID":"01F8MH1H7YV1Z7D2C8K2730QBF","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","currentSignInAt":"2021-09-08T09:30:53.972481Z","lastSignInAt":"2021-09-08T08:00:53.972481Z","chosenLanguages":["en"],"locale":"en","lastEmailedAt":"2021-09-08T09:05:53.972482Z","confirmationTokenSentAt":"2021-09-06T22:00:53.972483Z","confirmedAt":"2021-09-07T00:00:53.972482Z","moderator":false,"admin":false,"disabled":false,"approved":true}
{"type":"user","id":"01F8MH1VYJAE00TVVGMM5JNJ8X","createdAt":"2021-09-06T22:00:53.972485Z","email":"tortle.dude@example.org","accountID":"01F8MH5NBDF2MV7CTC4Q5128HF","encryptedPassword":"$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS","currentSignInAt":"2021-09-08T09:30:53.972485Z","lastSignInAt":"2021-09-08T08:00:53.972486Z","chosenLanguages":["en"],"locale":"en","lastEmailedAt":"2021-09-08T09:05:53.972487Z","confirmationTokenSentAt":"2021-09-06T22:00:53.972487Z","confirmedAt":"2021-09-07T00:00:53.972487Z","moderator":false,"admin":false,"disabled":false,"approved":true}
{"type":"instance","id":"01BZDDRPAB8J645ABY31HHF68Y","createdAt":"2021-09-08T10:00:54.763912Z","domain":"localhost:8080","title":"localhost:8080","uri":"http://localhost:8080","reputation":0}
```
### gotosocial admin import
This command can be used to import data from a file into your GoToSocial database.
If GoToSocial tables don't yet exist in the database, they will be created.
If any conflicts occur while importing (an already exists while attempting to import a specific account, for example), then the process will be aborted.
The file format should be a series of newline-separated JSON objects (see above).
`gotosocial admin import --help`:
```text
NAME:
gotosocial admin import - import data from a file into the database
USAGE:
gotosocial admin import [command options] [arguments...]
OPTIONS:
--path value the path of the file to import from/export to
--help, -h show help (default: false)
```
Example:
```bash
gotosocial admin import --path ./example.json
```

View file

@ -157,7 +157,7 @@ func (suite *RepliesGetTestSuite) TestGetRepliesNext() {
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) assert.NoError(suite.T(), err)
assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true\u0026only_other_accounts=false","items":"http://localhost:8080/users/1happyturtle/statuses/01FCQSQ667XHJ9AV9T27SJJSX5","next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true\u0026min_id=01FCQSQ667XHJ9AV9T27SJJSX5","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"}`, string(b)) assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true\u0026only_other_accounts=false","items":"http://localhost:8080/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0","next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true\u0026min_id=01FF25D5Q0DH7CHD57CTRS6WK0","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"}`, string(b))
// should be a Collection // should be a Collection
m := make(map[string]interface{}) m := make(map[string]interface{})
@ -188,7 +188,7 @@ func (suite *RepliesGetTestSuite) TestGetRepliesLast() {
// setup request // setup request
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies?only_other_accounts=false&page=true&min_id=01FCQSQ667XHJ9AV9T27SJJSX5", nil) // the endpoint we're hitting ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies?only_other_accounts=false&page=true&min_id=01FF25D5Q0DH7CHD57CTRS6WK0", nil) // the endpoint we're hitting
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
ctx.Request.Header.Set("Date", signedRequest.DateHeader) ctx.Request.Header.Set("Date", signedRequest.DateHeader)
@ -220,7 +220,7 @@ func (suite *RepliesGetTestSuite) TestGetRepliesLast() {
assert.NoError(suite.T(), err) assert.NoError(suite.T(), err)
fmt.Println(string(b)) fmt.Println(string(b))
assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true\u0026only_other_accounts=false\u0026min_id=01FCQSQ667XHJ9AV9T27SJJSX5","items":[],"next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"}`, string(b)) assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true\u0026only_other_accounts=false\u0026min_id=01FF25D5Q0DH7CHD57CTRS6WK0","items":[],"next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"}`, string(b))
// should be a Collection // should be a Collection
m := make(map[string]interface{}) m := make(map[string]interface{})

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package export package trans
import ( import (
"context" "context"

View file

@ -0,0 +1,56 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package trans
import (
"context"
"errors"
"fmt"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/cliactions"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db/bundb"
"github.com/superseriousbusiness/gotosocial/internal/trans"
)
// Import imports info from a file into the database
var Import cliactions.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
dbConn, err := bundb.NewBunDBService(ctx, c, log)
if err != nil {
return fmt.Errorf("error creating dbservice: %s", err)
}
importer := trans.NewImporter(dbConn, log)
path, ok := c.ExportCLIFlags[config.TransPathFlag]
if !ok {
return errors.New("no path set")
}
if err := dbConn.CreateAllTables(ctx); err != nil {
return err
}
if err := importer.Import(ctx, path); err != nil {
return err
}
return dbConn.Stop(ctx)
}

View file

@ -39,7 +39,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
"github.com/superseriousbusiness/gotosocial/internal/gotosocial" "github.com/superseriousbusiness/gotosocial/internal/gotosocial"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/oidc"
@ -51,32 +50,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/web" "github.com/superseriousbusiness/gotosocial/internal/web"
) )
var models []interface{} = []interface{}{
&gtsmodel.Account{},
&gtsmodel.Application{},
&gtsmodel.Block{},
&gtsmodel.DomainBlock{},
&gtsmodel.EmailDomainBlock{},
&gtsmodel.Follow{},
&gtsmodel.FollowRequest{},
&gtsmodel.MediaAttachment{},
&gtsmodel.Mention{},
&gtsmodel.Status{},
&gtsmodel.StatusToEmoji{},
&gtsmodel.StatusToTag{},
&gtsmodel.StatusFave{},
&gtsmodel.StatusBookmark{},
&gtsmodel.StatusMute{},
&gtsmodel.Tag{},
&gtsmodel.User{},
&gtsmodel.Emoji{},
&gtsmodel.Instance{},
&gtsmodel.Notification{},
&gtsmodel.RouterSession{},
&gtsmodel.Token{},
&gtsmodel.Client{},
}
// Start creates and starts a gotosocial server // Start creates and starts a gotosocial server
var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error { var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
dbService, err := bundb.NewBunDBService(ctx, c, log) dbService, err := bundb.NewBunDBService(ctx, c, log)
@ -84,10 +57,8 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log
return fmt.Errorf("error creating dbservice: %s", err) return fmt.Errorf("error creating dbservice: %s", err)
} }
for _, m := range models { if err := dbService.CreateAllTables(ctx); err != nil {
if err := dbService.CreateTable(ctx, m); err != nil { return fmt.Errorf("error creating database tables: %s", err)
return fmt.Errorf("table creation error: %s", err)
}
} }
if err := dbService.CreateInstanceAccount(ctx); err != nil { if err := dbService.CreateInstanceAccount(ctx); err != nil {

View file

@ -26,6 +26,10 @@ type Basic interface {
// For implementations that don't use tables, this can just return nil. // For implementations that don't use tables, this can just return nil.
CreateTable(ctx context.Context, i interface{}) Error CreateTable(ctx context.Context, i interface{}) Error
// CreateAllTables creates *all* tables necessary for the running of GoToSocial.
// Because it uses the 'if not exists' parameter it is safe to run against a GtS that's already been initialized.
CreateAllTables(ctx context.Context) Error
// DropTable drops the table for the given interface. // DropTable drops the table for the given interface.
// For implementations that don't use tables, this can just return nil. // For implementations that don't use tables, this can just return nil.
DropTable(ctx context.Context, i interface{}) Error DropTable(ctx context.Context, i interface{}) Error

View file

@ -24,6 +24,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/uptrace/bun" "github.com/uptrace/bun"
) )
@ -53,17 +54,8 @@ func (b *basicDB) GetWhere(ctx context.Context, where []db.Where, i interface{})
} }
q := b.conn.NewSelect().Model(i) q := b.conn.NewSelect().Model(i)
for _, w := range where {
if w.Value == nil { selectWhere(q, where)
q = q.Where("? IS NULL", bun.Ident(w.Key))
} else {
if w.CaseInsensitive {
q = q.Where("LOWER(?) = LOWER(?)", bun.Safe(w.Key), w.Value)
} else {
q = q.Where("? = ?", bun.Safe(w.Key), w.Value)
}
}
}
err := q.Scan(ctx) err := q.Scan(ctx)
return b.conn.ProcessError(err) return b.conn.ProcessError(err)
@ -97,9 +89,7 @@ func (b *basicDB) DeleteWhere(ctx context.Context, where []db.Where, i interface
NewDelete(). NewDelete().
Model(i) Model(i)
for _, w := range where { deleteWhere(q, where)
q = q.Where("? = ?", bun.Safe(w.Key), w.Value)
}
_, err := q.Exec(ctx) _, err := q.Exec(ctx)
return b.conn.ProcessError(err) return b.conn.ProcessError(err)
@ -128,17 +118,7 @@ func (b *basicDB) UpdateOneByID(ctx context.Context, id string, key string, valu
func (b *basicDB) UpdateWhere(ctx context.Context, where []db.Where, key string, value interface{}, i interface{}) db.Error { func (b *basicDB) UpdateWhere(ctx context.Context, where []db.Where, key string, value interface{}, i interface{}) db.Error {
q := b.conn.NewUpdate().Model(i) q := b.conn.NewUpdate().Model(i)
for _, w := range where { updateWhere(q, where)
if w.Value == nil {
q = q.Where("? IS NULL", bun.Ident(w.Key))
} else {
if w.CaseInsensitive {
q = q.Where("LOWER(?) = LOWER(?)", bun.Safe(w.Key), w.Value)
} else {
q = q.Where("? = ?", bun.Safe(w.Key), w.Value)
}
}
}
q = q.Set("? = ?", bun.Safe(key), value) q = q.Set("? = ?", bun.Safe(key), value)
@ -151,6 +131,40 @@ func (b *basicDB) CreateTable(ctx context.Context, i interface{}) db.Error {
return err return err
} }
func (b *basicDB) CreateAllTables(ctx context.Context) db.Error {
models := []interface{}{
&gtsmodel.Account{},
&gtsmodel.Application{},
&gtsmodel.Block{},
&gtsmodel.DomainBlock{},
&gtsmodel.EmailDomainBlock{},
&gtsmodel.Follow{},
&gtsmodel.FollowRequest{},
&gtsmodel.MediaAttachment{},
&gtsmodel.Mention{},
&gtsmodel.Status{},
&gtsmodel.StatusToEmoji{},
&gtsmodel.StatusToTag{},
&gtsmodel.StatusFave{},
&gtsmodel.StatusBookmark{},
&gtsmodel.StatusMute{},
&gtsmodel.Tag{},
&gtsmodel.User{},
&gtsmodel.Emoji{},
&gtsmodel.Instance{},
&gtsmodel.Notification{},
&gtsmodel.RouterSession{},
&gtsmodel.Token{},
&gtsmodel.Client{},
}
for _, i := range models {
if err := b.CreateTable(ctx, i); err != nil {
return err
}
}
return nil
}
func (b *basicDB) DropTable(ctx context.Context, i interface{}) db.Error { func (b *basicDB) DropTable(ctx context.Context, i interface{}) db.Error {
_, err := b.conn.NewDropTable().Model(i).IfExists().Exec(ctx) _, err := b.conn.NewDropTable().Model(i).IfExists().Exec(ctx)
return b.conn.ProcessError(err) return b.conn.ProcessError(err)

View file

@ -23,6 +23,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
) )
@ -42,7 +43,25 @@ func (suite *BasicTestSuite) TestGetAllStatuses() {
s := []*gtsmodel.Status{} s := []*gtsmodel.Status{}
err := suite.db.GetAll(context.Background(), &s) err := suite.db.GetAll(context.Background(), &s)
suite.NoError(err) suite.NoError(err)
suite.Len(s, 12) suite.Len(s, 13)
}
func (suite *BasicTestSuite) TestGetAllNotNull() {
where := []db.Where{{
Key: "domain",
Value: nil,
Not: true,
}}
a := []*gtsmodel.Account{}
err := suite.db.GetWhere(context.Background(), where, &a)
suite.NoError(err)
suite.NotEmpty(a)
for _, acct := range a {
suite.NotEmpty(acct.Domain)
}
} }
func TestBasicTestSuite(t *testing.T) { func TestBasicTestSuite(t *testing.T) {

View file

@ -240,13 +240,13 @@ func (s *statusDB) statusChildren(ctx context.Context, status *gtsmodel.Status,
} }
} }
// only do one loop if we only want direct children // if we're not only looking for direct children of status, then do the same children-finding
if onlyDirect { // operation for the found child status too.
return if !onlyDirect {
}
s.statusChildren(ctx, child, foundStatuses, false, minID) s.statusChildren(ctx, child, foundStatuses, false, minID)
} }
} }
}
func (s *statusDB) CountStatusReplies(ctx context.Context, status *gtsmodel.Status) (int, db.Error) { func (s *statusDB) CountStatusReplies(ctx context.Context, status *gtsmodel.Status) (int, db.Error) {
return s.conn.NewSelect().Model(&gtsmodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count(ctx) return s.conn.NewSelect().Model(&gtsmodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count(ctx)

View file

@ -104,6 +104,13 @@ func (suite *StatusTestSuite) TestGetStatusTwice() {
suite.Less(duration2, duration1) suite.Less(duration2, duration1)
} }
func (suite *StatusTestSuite) TestGetStatusChildren() {
targetStatus := suite.testStatuses["local_account_1_status_1"]
children, err := suite.db.GetStatusChildren(context.Background(), targetStatus, true, "")
suite.NoError(err)
suite.Len(children, 2)
}
func TestStatusTestSuite(t *testing.T) { func TestStatusTestSuite(t *testing.T) {
suite.Run(t, new(StatusTestSuite)) suite.Run(t, new(StatusTestSuite))
} }

View file

@ -19,6 +19,7 @@
package bundb package bundb
import ( import (
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/uptrace/bun" "github.com/uptrace/bun"
) )
@ -35,3 +36,65 @@ func whereEmptyOrNull(column string) func(*bun.SelectQuery) *bun.SelectQuery {
WhereOr("? = ''", bun.Ident(column)) WhereOr("? = ''", bun.Ident(column))
} }
} }
// updateWhere parses []db.Where and adds it to the given update query.
func updateWhere(q *bun.UpdateQuery, where []db.Where) {
for _, w := range where {
query, args := parseWhere(w)
q = q.Where(query, args...)
}
}
// selectWhere parses []db.Where and adds it to the given select query.
func selectWhere(q *bun.SelectQuery, where []db.Where) {
for _, w := range where {
query, args := parseWhere(w)
q = q.Where(query, args...)
}
}
// deleteWhere parses []db.Where and adds it to the given where query.
func deleteWhere(q *bun.DeleteQuery, where []db.Where) {
for _, w := range where {
query, args := parseWhere(w)
q = q.Where(query, args...)
}
}
// parseWhere looks through the options on a single db.Where entry, and
// returns the appropriate query string and arguments.
func parseWhere(w db.Where) (query string, args []interface{}) {
if w.Not {
if w.Value == nil {
query = "? IS NOT NULL"
args = []interface{}{bun.Ident(w.Key)}
return
}
if w.CaseInsensitive {
query = "LOWER(?) != LOWER(?)"
args = []interface{}{bun.Safe(w.Key), w.Value}
return
}
query = "? != ?"
args = []interface{}{bun.Safe(w.Key), w.Value}
return
}
if w.Value == nil {
query = "? IS NULL"
args = []interface{}{bun.Ident(w.Key)}
return
}
if w.CaseInsensitive {
query = "LOWER(?) = LOWER(?)"
args = []interface{}{bun.Safe(w.Key), w.Value}
return
}
query = "? = ?"
args = []interface{}{bun.Safe(w.Key), w.Value}
return
}

View file

@ -22,9 +22,13 @@ package db
type Where struct { type Where struct {
// The table to search on. // The table to search on.
Key string Key string
// The value that must be set. // The value to match.
Value interface{} Value interface{}
// Whether the value (if a string) should be case sensitive or not. // Whether the value (if a string) should be case sensitive or not.
// Defaults to false. // Defaults to false.
CaseInsensitive bool CaseInsensitive bool
// If set, reverse the where.
// `WHERE k = v` becomes `WHERE k != v`.
// `WHERE k IS NULL` becomes `WHERE k IS NOT NULL`
Not bool
} }

View file

@ -73,8 +73,8 @@ func (suite *GetTestSuite) TestGetDefault() {
suite.FailNow(err.Error()) suite.FailNow(err.Error())
} }
// we only have 12 statuses in the test suite // we only have 13 statuses in the test suite
suite.Len(statuses, 12) suite.Len(statuses, 13)
// statuses should be sorted highest to lowest ID // statuses should be sorted highest to lowest ID
var highest string var highest string
@ -166,8 +166,8 @@ func (suite *GetTestSuite) TestGetMinID() {
suite.FailNow(err.Error()) suite.FailNow(err.Error())
} }
// we should only get 5 statuses back, since we asked for a min ID that excludes some of our entries // we should only get 6 statuses back, since we asked for a min ID that excludes some of our entries
suite.Len(statuses, 5) suite.Len(statuses, 6)
// statuses should be sorted highest to lowest ID // statuses should be sorted highest to lowest ID
var highest string var highest string
@ -188,8 +188,8 @@ func (suite *GetTestSuite) TestGetSinceID() {
suite.FailNow(err.Error()) suite.FailNow(err.Error())
} }
// we should only get 5 statuses back, since we asked for a since ID that excludes some of our entries // we should only get 6 statuses back, since we asked for a since ID that excludes some of our entries
suite.Len(statuses, 5) suite.Len(statuses, 6)
// statuses should be sorted highest to lowest ID // statuses should be sorted highest to lowest ID
var highest string var highest string
@ -210,8 +210,8 @@ func (suite *GetTestSuite) TestGetSinceIDPrepareNext() {
suite.FailNow(err.Error()) suite.FailNow(err.Error())
} }
// we should only get 5 statuses back, since we asked for a since ID that excludes some of our entries // we should only get 6 statuses back, since we asked for a since ID that excludes some of our entries
suite.Len(statuses, 5) suite.Len(statuses, 6)
// statuses should be sorted highest to lowest ID // statuses should be sorted highest to lowest ID
var highest string var highest string

View file

@ -66,7 +66,7 @@ func (suite *IndexTestSuite) TestIndexBeforeLowID() {
// the oldest indexed post should be the lowest one we have in our testrig // the oldest indexed post should be the lowest one we have in our testrig
postID, err := suite.timeline.OldestIndexedPostID(context.Background()) postID, err := suite.timeline.OldestIndexedPostID(context.Background())
suite.NoError(err) suite.NoError(err)
suite.Equal("01F8MHAAY43M6RJ473VQFCVH37", postID) suite.Equal("01F8MHAMCHF6Y650WCRSCP4WMY", postID)
indexLength := suite.timeline.PostIndexLength(context.Background()) indexLength := suite.timeline.PostIndexLength(context.Background())
suite.Equal(10, indexLength) suite.Equal(10, indexLength)
@ -95,7 +95,7 @@ func (suite *IndexTestSuite) TestIndexBehindHighID() {
// the newest indexed post should be the highest one we have in our testrig // the newest indexed post should be the highest one we have in our testrig
postID, err := suite.timeline.NewestIndexedPostID(context.Background()) postID, err := suite.timeline.NewestIndexedPostID(context.Background())
suite.NoError(err) suite.NoError(err)
suite.Equal("01FCTA44PW9H1TB328S9AQXKDS", postID) suite.Equal("01FF25D5Q0DH7CHD57CTRS6WK0", postID)
// indexLength should be 10 because that's all this user has hometimelineable // indexLength should be 10 because that's all this user has hometimelineable
indexLength := suite.timeline.PostIndexLength(context.Background()) indexLength := suite.timeline.PostIndexLength(context.Background())

View file

@ -67,9 +67,9 @@ func (suite *ManagerTestSuite) TestManagerIntegration() {
err = suite.manager.PrepareXFromTop(context.Background(), testAccount.ID, 20) err = suite.manager.PrepareXFromTop(context.Background(), testAccount.ID, 20)
suite.NoError(err) suite.NoError(err)
// local_account_1 can see 12 statuses out of the testrig statuses in its home timeline // local_account_1 can see 13 statuses out of the testrig statuses in its home timeline
indexedLen = suite.manager.GetIndexedLength(context.Background(), testAccount.ID) indexedLen = suite.manager.GetIndexedLength(context.Background(), testAccount.ID)
suite.Equal(12, indexedLen) suite.Equal(13, indexedLen)
// oldest should now be set // oldest should now be set
oldestIndexed, err = suite.manager.GetOldestIndexedID(context.Background(), testAccount.ID) oldestIndexed, err = suite.manager.GetOldestIndexedID(context.Background(), testAccount.ID)
@ -79,7 +79,7 @@ func (suite *ManagerTestSuite) TestManagerIntegration() {
// get hometimeline // get hometimeline
statuses, err := suite.manager.HomeTimeline(context.Background(), testAccount.ID, "", "", "", 20, false) statuses, err := suite.manager.HomeTimeline(context.Background(), testAccount.ID, "", "", "", 20, false)
suite.NoError(err) suite.NoError(err)
suite.Len(statuses, 12) suite.Len(statuses, 13)
// now wipe the last status from all timelines, as though it had been deleted by the owner // now wipe the last status from all timelines, as though it had been deleted by the owner
err = suite.manager.WipeStatusFromAllTimelines(context.Background(), "01F8MH75CBF9JFX4ZAD54N0W0R") err = suite.manager.WipeStatusFromAllTimelines(context.Background(), "01F8MH75CBF9JFX4ZAD54N0W0R")
@ -87,7 +87,7 @@ func (suite *ManagerTestSuite) TestManagerIntegration() {
// timeline should be shorter // timeline should be shorter
indexedLen = suite.manager.GetIndexedLength(context.Background(), testAccount.ID) indexedLen = suite.manager.GetIndexedLength(context.Background(), testAccount.ID)
suite.Equal(11, indexedLen) suite.Equal(12, indexedLen)
// oldest should now be different // oldest should now be different
oldestIndexed, err = suite.manager.GetOldestIndexedID(context.Background(), testAccount.ID) oldestIndexed, err = suite.manager.GetOldestIndexedID(context.Background(), testAccount.ID)
@ -101,7 +101,7 @@ func (suite *ManagerTestSuite) TestManagerIntegration() {
// timeline should be shorter // timeline should be shorter
indexedLen = suite.manager.GetIndexedLength(context.Background(), testAccount.ID) indexedLen = suite.manager.GetIndexedLength(context.Background(), testAccount.ID)
suite.Equal(10, indexedLen) suite.Equal(11, indexedLen)
// oldest should now be different // oldest should now be different
oldestIndexed, err = suite.manager.GetOldestIndexedID(context.Background(), testAccount.ID) oldestIndexed, err = suite.manager.GetOldestIndexedID(context.Background(), testAccount.ID)
@ -112,9 +112,9 @@ func (suite *ManagerTestSuite) TestManagerIntegration() {
err = suite.manager.WipeStatusesFromAccountID(context.Background(), testAccount.ID, suite.testAccounts["local_account_2"].ID) err = suite.manager.WipeStatusesFromAccountID(context.Background(), testAccount.ID, suite.testAccounts["local_account_2"].ID)
suite.NoError(err) suite.NoError(err)
// timeline should be empty now // timeline should be shorter
indexedLen = suite.manager.GetIndexedLength(context.Background(), testAccount.ID) indexedLen = suite.manager.GetIndexedLength(context.Background(), testAccount.ID)
suite.Equal(5, indexedLen) suite.Equal(6, indexedLen)
// ingest 1 into the timeline // ingest 1 into the timeline
status1 := suite.testStatuses["admin_account_status_1"] status1 := suite.testStatuses["admin_account_status_1"]
@ -130,7 +130,7 @@ func (suite *ManagerTestSuite) TestManagerIntegration() {
// timeline should be longer now // timeline should be longer now
indexedLen = suite.manager.GetIndexedLength(context.Background(), testAccount.ID) indexedLen = suite.manager.GetIndexedLength(context.Background(), testAccount.ID)
suite.Equal(7, indexedLen) suite.Equal(8, indexedLen)
// try to ingest status 2 again // try to ingest status 2 again
ingested, err = suite.manager.IngestAndPrepare(context.Background(), status2, testAccount.ID) ingested, err = suite.manager.IngestAndPrepare(context.Background(), status2, testAccount.ID)

View file

@ -37,7 +37,7 @@ func newDecoder(target interface{}) (*mapstructure.Decoder, error) {
return mapstructure.NewDecoder(decoderConfig) return mapstructure.NewDecoder(decoderConfig)
} }
func (i *importer) accountDecode(e transmodel.TransEntry) (*transmodel.Account, error) { func (i *importer) accountDecode(e transmodel.Entry) (*transmodel.Account, error) {
a := &transmodel.Account{} a := &transmodel.Account{}
if err := i.simpleDecode(e, a); err != nil { if err := i.simpleDecode(e, a); err != nil {
return nil, err return nil, err
@ -70,7 +70,7 @@ func (i *importer) accountDecode(e transmodel.TransEntry) (*transmodel.Account,
return a, nil return a, nil
} }
func (i *importer) blockDecode(e transmodel.TransEntry) (*transmodel.Block, error) { func (i *importer) blockDecode(e transmodel.Entry) (*transmodel.Block, error) {
b := &transmodel.Block{} b := &transmodel.Block{}
if err := i.simpleDecode(e, b); err != nil { if err := i.simpleDecode(e, b); err != nil {
return nil, err return nil, err
@ -79,7 +79,7 @@ func (i *importer) blockDecode(e transmodel.TransEntry) (*transmodel.Block, erro
return b, nil return b, nil
} }
func (i *importer) domainBlockDecode(e transmodel.TransEntry) (*transmodel.DomainBlock, error) { func (i *importer) domainBlockDecode(e transmodel.Entry) (*transmodel.DomainBlock, error) {
b := &transmodel.DomainBlock{} b := &transmodel.DomainBlock{}
if err := i.simpleDecode(e, b); err != nil { if err := i.simpleDecode(e, b); err != nil {
return nil, err return nil, err
@ -88,7 +88,7 @@ func (i *importer) domainBlockDecode(e transmodel.TransEntry) (*transmodel.Domai
return b, nil return b, nil
} }
func (i *importer) followDecode(e transmodel.TransEntry) (*transmodel.Follow, error) { func (i *importer) followDecode(e transmodel.Entry) (*transmodel.Follow, error) {
f := &transmodel.Follow{} f := &transmodel.Follow{}
if err := i.simpleDecode(e, f); err != nil { if err := i.simpleDecode(e, f); err != nil {
return nil, err return nil, err
@ -97,7 +97,7 @@ func (i *importer) followDecode(e transmodel.TransEntry) (*transmodel.Follow, er
return f, nil return f, nil
} }
func (i *importer) followRequestDecode(e transmodel.TransEntry) (*transmodel.FollowRequest, error) { func (i *importer) followRequestDecode(e transmodel.Entry) (*transmodel.FollowRequest, error) {
f := &transmodel.FollowRequest{} f := &transmodel.FollowRequest{}
if err := i.simpleDecode(e, f); err != nil { if err := i.simpleDecode(e, f); err != nil {
return nil, err return nil, err
@ -106,7 +106,7 @@ func (i *importer) followRequestDecode(e transmodel.TransEntry) (*transmodel.Fol
return f, nil return f, nil
} }
func (i *importer) instanceDecode(e transmodel.TransEntry) (*transmodel.Instance, error) { func (i *importer) instanceDecode(e transmodel.Entry) (*transmodel.Instance, error) {
inst := &transmodel.Instance{} inst := &transmodel.Instance{}
if err := i.simpleDecode(e, inst); err != nil { if err := i.simpleDecode(e, inst); err != nil {
return nil, err return nil, err
@ -115,7 +115,7 @@ func (i *importer) instanceDecode(e transmodel.TransEntry) (*transmodel.Instance
return inst, nil return inst, nil
} }
func (i *importer) userDecode(e transmodel.TransEntry) (*transmodel.User, error) { func (i *importer) userDecode(e transmodel.Entry) (*transmodel.User, error) {
u := &transmodel.User{} u := &transmodel.User{}
if err := i.simpleDecode(e, u); err != nil { if err := i.simpleDecode(e, u); err != nil {
return nil, err return nil, err
@ -124,7 +124,7 @@ func (i *importer) userDecode(e transmodel.TransEntry) (*transmodel.User, error)
return u, nil return u, nil
} }
func (i *importer) simpleDecode(entry transmodel.TransEntry, target interface{}) error { func (i *importer) simpleDecode(entry transmodel.Entry, target interface{}) error {
decoder, err := newDecoder(target) decoder, err := newDecoder(target)
if err != nil { if err != nil {
return fmt.Errorf("simpleDecode: error creating decoder: %s", err) return fmt.Errorf("simpleDecode: error creating decoder: %s", err)

View file

@ -93,7 +93,7 @@ func (e *exporter) exportDomainBlocks(ctx context.Context, f *os.File) ([]*trans
} }
for _, b := range domainBlocks { for _, b := range domainBlocks {
b.Type = transmodel.TransBlock b.Type = transmodel.TransDomainBlock
if err := e.simpleEncode(ctx, f, b, b.ID); err != nil { if err := e.simpleEncode(ctx, f, b, b.ID); err != nil {
return nil, fmt.Errorf("exportBlocks: error encoding domain block: %s", err) return nil, fmt.Errorf("exportBlocks: error encoding domain block: %s", err)
} }

View file

@ -25,6 +25,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
) )
// Exporter wraps functionality for exporting entries from the database to a file.
type Exporter interface { type Exporter interface {
ExportMinimal(ctx context.Context, path string) error ExportMinimal(ctx context.Context, path string) error
} }
@ -35,6 +36,7 @@ type exporter struct {
writtenIDs map[string]bool writtenIDs map[string]bool
} }
// NewExporter returns a new Exporter that will use the given db and logger.
func NewExporter(db db.DB, log *logrus.Logger) Exporter { func NewExporter(db db.DB, log *logrus.Logger) Exporter {
return &exporter{ return &exporter{
db: db, db: db,

View file

@ -136,5 +136,15 @@ func (e *exporter) ExportMinimal(ctx context.Context, path string) error {
return fmt.Errorf("ExportMinimal: error exporting instances: %s", err) return fmt.Errorf("ExportMinimal: error exporting instances: %s", err)
} }
// export all SUSPENDED accounts to make sure the suspension sticks across db migration etc
whereSuspended := []db.Where{{
Key: "suspended_at",
Not: true,
Value: nil,
}}
if _, err := e.exportAccounts(ctx, whereSuspended, f); err != nil {
return fmt.Errorf("ExportMinimal: error exporting suspended accounts: %s", err)
}
return neatClose(f) return neatClose(f)
} }

View file

@ -30,37 +30,41 @@ import (
) )
func (i *importer) Import(ctx context.Context, path string) error { func (i *importer) Import(ctx context.Context, path string) error {
if path == "" {
return errors.New("Export: path empty")
}
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
return fmt.Errorf("ImportMinimal: error opening file %s: %s", path, err) return fmt.Errorf("Import: couldn't export to %s: %s", path, err)
} }
decoder := json.NewDecoder(f) decoder := json.NewDecoder(f)
decoder.UseNumber() decoder.UseNumber()
for { for {
entry := transmodel.TransEntry{} entry := transmodel.Entry{}
err := decoder.Decode(&entry) err := decoder.Decode(&entry)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
i.log.Infof("ImportMinimal: reached end of file") i.log.Infof("Import: reached end of file")
return neatClose(f) return neatClose(f)
} }
return fmt.Errorf("ImportMinimal: error decoding in readLoop: %s", err) return fmt.Errorf("Import: error decoding in readLoop: %s", err)
} }
if err := i.inputEntry(ctx, entry); err != nil { if err := i.inputEntry(ctx, entry); err != nil {
return fmt.Errorf("ImportMinimal: error inputting entry: %s", err) return fmt.Errorf("Import: error inputting entry: %s", err)
} }
} }
} }
func (i *importer) inputEntry(ctx context.Context, entry transmodel.TransEntry) error { func (i *importer) inputEntry(ctx context.Context, entry transmodel.Entry) error {
t, ok := entry[transmodel.TypeKey].(string) t, ok := entry[transmodel.TypeKey].(string)
if !ok { if !ok {
return errors.New("inputEntry: could not derive entry type: missing or malformed 'type' key in json") return errors.New("inputEntry: could not derive entry type: missing or malformed 'type' key in json")
} }
switch transmodel.TransType(t) { switch transmodel.Type(t) {
case transmodel.TransAccount: case transmodel.TransAccount:
account, err := i.accountDecode(entry) account, err := i.accountDecode(entry)
if err != nil { if err != nil {
@ -84,12 +88,12 @@ func (i *importer) inputEntry(ctx context.Context, entry transmodel.TransEntry)
case transmodel.TransDomainBlock: case transmodel.TransDomainBlock:
block, err := i.domainBlockDecode(entry) block, err := i.domainBlockDecode(entry)
if err != nil { if err != nil {
return fmt.Errorf("inputEntry: error decoding entry into block: %s", err) return fmt.Errorf("inputEntry: error decoding entry into domain block: %s", err)
} }
if err := i.putInDB(ctx, block); err != nil { if err := i.putInDB(ctx, block); err != nil {
return fmt.Errorf("inputEntry: error adding block to database: %s", err) return fmt.Errorf("inputEntry: error adding domain block to database: %s", err)
} }
i.log.Infof("inputEntry: added block with id %s", block.ID) i.log.Infof("inputEntry: added domain block with id %s", block.ID)
return nil return nil
case transmodel.TransFollow: case transmodel.TransFollow:
follow, err := i.followDecode(entry) follow, err := i.followDecode(entry)

View file

@ -72,6 +72,18 @@ func (suite *ImportMinimalTestSuite) TestImportMinimalOK() {
err = newDB.GetAll(ctx, &blocks) err = newDB.GetAll(ctx, &blocks)
suite.NoError(err) suite.NoError(err)
suite.NotEmpty(blocks) suite.NotEmpty(blocks)
// we should have some follows in the database
follows := []*gtsmodel.Follow{}
err = newDB.GetAll(ctx, &follows)
suite.NoError(err)
suite.NotEmpty(follows)
// we should have some domain blocks in the database
domainBlocks := []*gtsmodel.DomainBlock{}
err = newDB.GetAll(ctx, &domainBlocks)
suite.NoError(err)
suite.NotEmpty(domainBlocks)
} }
func TestImportMinimalTestSuite(t *testing.T) { func TestImportMinimalTestSuite(t *testing.T) {

View file

@ -25,6 +25,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
) )
// Importer wraps functionality for importing entries from a file into the database.
type Importer interface { type Importer interface {
Import(ctx context.Context, path string) error Import(ctx context.Context, path string) error
} }
@ -34,6 +35,7 @@ type importer struct {
log *logrus.Logger log *logrus.Logger
} }
// NewImporter returns a new Importer interface that uses the given db and logger.
func NewImporter(db db.DB, log *logrus.Logger) Importer { func NewImporter(db db.DB, log *logrus.Logger) Importer {
return &importer{ return &importer{
db: db, db: db,

View file

@ -25,26 +25,28 @@ import (
// Account represents the minimum viable representation of an account for export/import. // Account represents the minimum viable representation of an account for export/import.
type Account struct { type Account struct {
Type TransType `json:"type" bun:"-"` Type Type `json:"type" bun:"-"`
ID string `json:"id"` ID string `json:"id" bun:",nullzero"`
CreatedAt *time.Time `json:"createdAt"` CreatedAt *time.Time `json:"createdAt" bun:",nullzero"`
Username string `json:"username"` Username string `json:"username" bun:",nullzero"`
Domain string `json:"domain,omitempty" bun:",nullzero"` Domain string `json:"domain,omitempty" bun:",nullzero"`
HeaderRemoteURL string `json:"headerRemoteURL,omitempty" bun:",nullzero"`
AvatarRemoteURL string `json:"avatarRemoteURL,omitempty" bun:",nullzero"`
Locked bool `json:"locked"` Locked bool `json:"locked"`
Language string `json:"language,omitempty"` Language string `json:"language,omitempty" bun:",nullzero"`
URI string `json:"uri"` URI string `json:"uri" bun:",nullzero"`
URL string `json:"url"` URL string `json:"url" bun:",nullzero"`
InboxURI string `json:"inboxURI"` InboxURI string `json:"inboxURI" bun:",nullzero"`
OutboxURI string `json:"outboxURI"` OutboxURI string `json:"outboxURI" bun:",nullzero"`
FollowingURI string `json:"followingUri"` FollowingURI string `json:"followingUri" bun:",nullzero"`
FollowersURI string `json:"followersUri"` FollowersURI string `json:"followersUri" bun:",nullzero"`
FeaturedCollectionURI string `json:"featuredCollectionUri"` FeaturedCollectionURI string `json:"featuredCollectionUri" bun:",nullzero"`
ActorType string `json:"actorType"` ActorType string `json:"actorType" bun:",nullzero"`
PrivateKey *rsa.PrivateKey `json:"-" mapstructure:"-"` PrivateKey *rsa.PrivateKey `json:"-" mapstructure:"-"`
PrivateKeyString string `json:"privateKey,omitempty" bun:"-" mapstructure:"privateKey"` PrivateKeyString string `json:"privateKey,omitempty" bun:"-" mapstructure:"privateKey" bun:",nullzero"`
PublicKey *rsa.PublicKey `json:"-" mapstructure:"-"` PublicKey *rsa.PublicKey `json:"-" mapstructure:"-"`
PublicKeyString string `json:"publicKey,omitempty" bun:"-" mapstructure:"publicKey"` PublicKeyString string `json:"publicKey,omitempty" bun:"-" mapstructure:"publicKey" bun:",nullzero"`
PublicKeyURI string `json:"publicKeyUri"` PublicKeyURI string `json:"publicKeyUri" bun:",nullzero"`
SuspendedAt *time.Time `json:"suspendedAt,omitempty"` SuspendedAt *time.Time `json:"suspendedAt,omitempty" bun:",nullzero"`
SuspensionOrigin string `json:"suspensionOrigin,omitempty" bun:",nullzero"` SuspensionOrigin string `json:"suspensionOrigin,omitempty" bun:",nullzero"`
} }

View file

@ -20,11 +20,12 @@ package trans
import "time" import "time"
// Block represents an account block as serialized in an exported file.
type Block struct { type Block struct {
Type TransType `json:"type" bun:"-"` Type Type `json:"type" bun:"-"`
ID string `json:"id"` ID string `json:"id" bun:",nullzero"`
CreatedAt *time.Time `json:"createdAt"` CreatedAt *time.Time `json:"createdAt" bun:",nullzero"`
URI string `json:"uri"` URI string `json:"uri" bun:",nullzero"`
AccountID string `json:"accountId"` AccountID string `json:"accountId" bun:",nullzero"`
TargetAccountID string `json:"targetAccountId"` TargetAccountID string `json:"targetAccountId" bun:",nullzero"`
} }

View file

@ -20,13 +20,15 @@ package trans
import "time" import "time"
// DomainBlock represents a domain block as serialized in an exported file.
type DomainBlock struct { type DomainBlock struct {
Type TransType `json:"type" bun:"-"` Type Type `json:"type" bun:"-"`
ID string `json:"id"` ID string `json:"id" bun:",nullzero"`
CreatedAt *time.Time `json:"createdAt"` CreatedAt *time.Time `json:"createdAt" bun:",nullzero"`
CreatedByAccountID string `json:"createdByAccountID"` Domain string `json:"domain" bun:",nullzero"`
PrivateComment string `json:"privateComment,omitempty"` CreatedByAccountID string `json:"createdByAccountID" bun:",nullzero"`
PublicComment string `json:"publicComment,omitempty"` PrivateComment string `json:"privateComment,omitempty" bun:",nullzero"`
Obfuscate bool `json:"obfuscate"` PublicComment string `json:"publicComment,omitempty" bun:",nullzero"`
SubscriptionID string `json:"subscriptionID,omitempty"` Obfuscate bool `json:"obfuscate" bun:",nullzero"`
SubscriptionID string `json:"subscriptionID,omitempty" bun:",nullzero"`
} }

View file

@ -20,11 +20,12 @@ package trans
import "time" import "time"
// Follow represents an account follow as serialized in an export file.
type Follow struct { type Follow struct {
Type TransType `json:"type" bun:"-"` Type Type `json:"type" bun:"-"`
ID string `json:"id"` ID string `json:"id" bun:",nullzero"`
CreatedAt *time.Time `json:"createdAt"` CreatedAt *time.Time `json:"createdAt" bun:",nullzero"`
URI string `json:"uri"` URI string `json:"uri" bun:",nullzero"`
AccountID string `json:"accountId"` AccountID string `json:"accountId" bun:",nullzero"`
TargetAccountID string `json:"targetAccountId"` TargetAccountID string `json:"targetAccountId" bun:",nullzero"`
} }

View file

@ -20,11 +20,12 @@ package trans
import "time" import "time"
// FollowRequest represents an account follow request as serialized in an export file.
type FollowRequest struct { type FollowRequest struct {
Type TransType `json:"type" bun:"-"` Type Type `json:"type" bun:"-"`
ID string `json:"id"` ID string `json:"id" bun:",nullzero"`
CreatedAt *time.Time `json:"createdAt"` CreatedAt *time.Time `json:"createdAt" bun:",nullzero"`
URI string `json:"uri"` URI string `json:"uri" bun:",nullzero"`
AccountID string `json:"accountId"` AccountID string `json:"accountId" bun:",nullzero"`
TargetAccountID string `json:"targetAccountId"` TargetAccountID string `json:"targetAccountId" bun:",nullzero"`
} }

View file

@ -22,13 +22,14 @@ import (
"time" "time"
) )
// Instance represents an instance entry as serialized in an export file.
type Instance struct { type Instance struct {
Type TransType `json:"type" bun:"-"` Type Type `json:"type" bun:"-"`
ID string `json:"id"` ID string `json:"id" bun:",nullzero"`
CreatedAt *time.Time `json:"createdAt"` CreatedAt *time.Time `json:"createdAt" bun:",nullzero"`
Domain string `json:"domain"` Domain string `json:"domain" bun:",nullzero"`
Title string `json:"title,omitempty" bun:",nullzero"` Title string `json:"title,omitempty" bun:",nullzero"`
URI string `json:"uri"` URI string `json:"uri" bun:",nullzero"`
SuspendedAt *time.Time `json:"suspendedAt,omitempty" bun:",nullzero"` SuspendedAt *time.Time `json:"suspendedAt,omitempty" bun:",nullzero"`
DomainBlockID string `json:"domainBlockID,omitempty" bun:",nullzero"` DomainBlockID string `json:"domainBlockID,omitempty" bun:",nullzero"`
ShortDescription string `json:"shortDescription,omitempty" bun:",nullzero"` ShortDescription string `json:"shortDescription,omitempty" bun:",nullzero"`

View file

@ -18,21 +18,24 @@
package trans package trans
// TypeKey should be set on a TransEntry to indicate the type of entry it is.
const TypeKey = "type" const TypeKey = "type"
// TransType describes the type of a trans entry, and how it should be read/serialized. // Type describes the type of a trans entry, and how it should be read/serialized.
type TransType string type Type string
// Type of the trans entry. Describes how it should be read from file. // Type of the trans entry. Describes how it should be read from file.
const ( const (
TransAccount TransType = "account" TransAccount Type = "account"
TransBlock TransType = "block" TransBlock Type = "block"
TransDomainBlock TransType = "domainBlock" TransDomainBlock Type = "domainBlock"
TransEmailDomainBlock TransType = "emailDomainBlock" TransEmailDomainBlock Type = "emailDomainBlock"
TransFollow TransType = "follow" TransFollow Type = "follow"
TransFollowRequest TransType = "followRequest" TransFollowRequest Type = "followRequest"
TransInstance TransType = "instance" TransInstance Type = "instance"
TransUser TransType = "user" TransUser Type = "user"
) )
type TransEntry map[string]interface{} // Entry is used for deserializing trans entries into a rough interface so that
// the TypeKey can be fetched, before continuing with full parsing.
type Entry map[string]interface{}

View file

@ -22,13 +22,14 @@ import (
"time" "time"
) )
// User represents a local instance user as serialized to an export file.
type User struct { type User struct {
Type TransType `json:"type" bun:"-"` Type Type `json:"type" bun:"-"`
ID string `json:"id"` ID string `json:"id" bun:",nullzero"`
CreatedAt *time.Time `json:"createdAt"` CreatedAt *time.Time `json:"createdAt" bun:",nullzero"`
Email string `json:"email,omitempty" bun:",nullzero"` Email string `json:"email,omitempty" bun:",nullzero"`
AccountID string `json:"accountID"` AccountID string `json:"accountID" bun:",nullzero"`
EncryptedPassword string `json:"encryptedPassword"` EncryptedPassword string `json:"encryptedPassword" bun:",nullzero"`
CurrentSignInAt *time.Time `json:"currentSignInAt,omitempty" bun:",nullzero"` CurrentSignInAt *time.Time `json:"currentSignInAt,omitempty" bun:",nullzero"`
LastSignInAt *time.Time `json:"lastSignInAt,omitempty" bun:",nullzero"` LastSignInAt *time.Time `json:"lastSignInAt,omitempty" bun:",nullzero"`
InviteID string `json:"inviteID,omitempty" bun:",nullzero"` InviteID string `json:"inviteID,omitempty" bun:",nullzero"`

View file

@ -123,6 +123,12 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) {
} }
} }
for _, v := range NewTestDomainBlocks() {
if err := db.Put(ctx, v); err != nil {
logrus.Panic(err)
}
}
for _, v := range NewTestUsers() { for _, v := range NewTestUsers() {
if err := db.Put(ctx, v); err != nil { if err := db.Put(ctx, v); err != nil {
logrus.Panic(err) logrus.Panic(err)

View file

@ -736,6 +736,19 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
} }
} }
func NewTestDomainBlocks() map[string]*gtsmodel.DomainBlock {
return map[string]*gtsmodel.DomainBlock{
"replyguys.com": {
ID: "01FF22EQM7X8E3RX1XGPN7S87D",
Domain: "replyguys.com",
CreatedByAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
PrivateComment: "i blocked this domain because they keep replying with pushy + unwarranted linux advice",
PublicComment: "reply-guying to tech posts",
Obfuscate: false,
},
}
}
type filenames struct { type filenames struct {
Original string Original string
Small string Small string
@ -836,6 +849,33 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
}, },
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"admin_account_status_3": {
ID: "01FF25D5Q0DH7CHD57CTRS6WK0",
URI: "http://localhost:8080/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0",
URL: "http://localhost:8080/@admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0",
Content: "hi @the_mighty_zork welcome to the instance!",
CreatedAt: time.Now().Add(-46 * time.Hour),
UpdatedAt: time.Now().Add(-46 * time.Hour),
Local: true,
AccountURI: "http://localhost:8080/users/admin",
MentionIDs: []string{"01FF26A6BGEKCZFWNEHXB2ZZ6M"},
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
InReplyToID: "01F8MHAMCHF6Y650WCRSCP4WMY",
InReplyToAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
InReplyToURI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
BoostOfID: "",
Visibility: gtsmodel.VisibilityPublic,
Sensitive: false,
Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
VisibilityAdvanced: gtsmodel.VisibilityAdvanced{
Federated: true,
Boostable: true,
Replyable: true,
Likeable: true,
},
ActivityStreamsType: ap.ObjectNote,
},
"local_account_1_status_1": { "local_account_1_status_1": {
ID: "01F8MHAMCHF6Y650WCRSCP4WMY", ID: "01F8MHAMCHF6Y650WCRSCP4WMY",
URI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY", URI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
@ -1149,6 +1189,18 @@ func NewTestMentions() map[string]*gtsmodel.Mention {
TargetAccountURI: "http://localhost:8080/users/the_mighty_zork", TargetAccountURI: "http://localhost:8080/users/the_mighty_zork",
TargetAccountURL: "http://localhost:8080/@the_mighty_zork", TargetAccountURL: "http://localhost:8080/@the_mighty_zork",
}, },
"admin_account_mention_zork": {
ID: "01FF26A6BGEKCZFWNEHXB2ZZ6M",
StatusID: "01FF25D5Q0DH7CHD57CTRS6WK0",
CreatedAt: time.Now().Add(-46 * time.Hour),
UpdatedAt: time.Now().Add(-46 * time.Hour),
OriginAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
OriginAccountURI: "http://localhost:8080/users/admin",
TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
NameString: "@the_mighty_zork",
TargetAccountURI: "http://localhost:8080/users/the_mighty_zork",
TargetAccountURL: "http://localhost:8080/@the_mighty_zork",
},
} }
} }
@ -1387,7 +1439,7 @@ func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[strin
DateHeader: date, DateHeader: date,
} }
target = URLMustParse(statuses["local_account_1_status_1"].URI + "/replies?only_other_accounts=false&page=true&min_id=01FCQSQ667XHJ9AV9T27SJJSX5") target = URLMustParse(statuses["local_account_1_status_1"].URI + "/replies?only_other_accounts=false&page=true&min_id=01FF25D5Q0DH7CHD57CTRS6WK0")
sig, digest, date = getSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target) sig, digest, date = getSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target)
fossSatanDereferenceLocalAccount1Status1RepliesLast := ActivityWithSignature{ fossSatanDereferenceLocalAccount1Status1RepliesLast := ActivityWithSignature{
SignatureHeader: sig, SignatureHeader: sig,