多租户更改
@@ -0,0 +1,54 @@
|
||||
# Utility to initialize or upgrade `tinode` DB
|
||||
|
||||
This utility initializes the `tinode` database (or upgrades an existing DB from an earlier version) and optionally loads it with data. To force database reset use command line option `--reset=true`.
|
||||
|
||||
## Build the package:
|
||||
|
||||
- **RethinkDB**
|
||||
`go build -tags rethinkdb` or `go build -i -tags rethinkdb` to automatically install missing dependencies.
|
||||
|
||||
- **MySQL**
|
||||
`go build -tags mysql` or `go build -i -tags mysql` to automatically install missing dependencies.
|
||||
|
||||
- **MongoDB**
|
||||
`go build -tags mongodb` or `go build -i -tags mongodb` to automatically install missing dependencies.
|
||||
|
||||
- **PostgreSQL**
|
||||
`go build -tags postgres` or `go build -i -tags postgres` to automatically install missing dependencies.
|
||||
|
||||
|
||||
## Run
|
||||
|
||||
Run from the command line.
|
||||
|
||||
`tinode-db [parameters]`
|
||||
|
||||
Command line parameters:
|
||||
- `--reset`: delete the database then re-create it in a blank state; it has no effect if the database does not exist.
|
||||
- `--upgrade`: upgrade database from an earlier version retaining all the data; make sure to backup the DB before upgrading.
|
||||
- `--no_init`: check that database exists but don't create it if missing.
|
||||
- `--data=FILENAME`: fill `tinode` database with data from the provided file. See [data.json](data.json).
|
||||
- `--config=FILENAME`: load configuration from FILENAME. Example config is included as [tinode.conf](tinode.conf).
|
||||
- `--make_root=USER_ID`: promote an existing user to root user, `USER_ID` of the form `usrAbCDef123`.
|
||||
- `--add_root=USERNAME[:PASSWORD]`: create a new user account and make it root; if password is missing, a strong password will be generated.
|
||||
|
||||
Configuration file options:
|
||||
- `uid_key` is a base64-encoded 16 byte XTEA encryption key to (weakly) encrypt object IDs so they don't appear sequential. You probably want to use your own key in production.
|
||||
- `store_config.adapters.mysql` and `store_config.adapters.rethinkdb` are database-specific sections:
|
||||
- `database` is the name of the database to generate.
|
||||
- `addresses` is RethinkDB/MongoDB's host and port number to connect to. An array of hosts can be provided as well `["host1", "host2"]`.
|
||||
- `dsn` is MySQL's Data Source Name.
|
||||
- `replica_set` is MongoDB's Replicaset name.
|
||||
|
||||
The `uid_key` is only used if the sample data is being loaded. It should match the key of a production server and should be kept private.
|
||||
|
||||
The default `data.json` file creates six users with user names `alice`, `bob`, `carol`, `dave`, `frank`, and `tino` (chat bot user). Passwords are the same as the user names with 123 appended, e.g. user `alice` gets password `alice123`; `tino` gets a randomly generated password. It also creates three group topics, and multiple peer to peer topics. Users are subscribed to topics and to each other. All topics are randomly filled with messages.
|
||||
|
||||
Avatar photos curtesy of https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
|
||||
|
||||
## Links:
|
||||
|
||||
* [RethinkDB schema](https://github.com/tinode/chat/tree/master/server/db/rethinkdb/schema.md)
|
||||
* [MySQL schema](https://github.com/tinode/chat/tree/master/server/db/mysql/schema.sql)
|
||||
* [MongoDB schema](https://github.com/tinode/chat/tree/master/server/db/mongodb/schema.md)
|
||||
* [PostgreSQL schema](https://github.com/tinode/chat/tree/master/server/db/postgres/schema.sql)
|
||||
|
||||
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Credential extractor. Tino the Chatbot is created with a random password. The password is written
|
||||
# to stdout by tinode-db. The script converts it to chatbot's authentication cookie.
|
||||
|
||||
# The script takes a string like 'usr;tino;usrImlot_X9vAc;cOuTvzVa' (ignored;login;user_id;password)
|
||||
# and formats it into a json chatbot's authentication cookie like
|
||||
# '{"schema": "basic", "secret": "username:password", "user": "user_id"}'.
|
||||
|
||||
COOKIE_FILE=$@
|
||||
|
||||
while read line; do
|
||||
IFS=';' read -r -a parts <<< "$line"
|
||||
if [ ${#parts[@]} -eq 0 ] ; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# If the name of the cookie file is given, write to file
|
||||
# Otherwise write to stdout
|
||||
if [ "$COOKIE_FILE" ]; then
|
||||
exec 3>"$COOKIE_FILE"
|
||||
else
|
||||
exec 3>&1
|
||||
fi
|
||||
|
||||
echo "{\"schema\": \"basic\", \"secret\": \"${parts[1]}:${parts[3]}\", \"user\": \"${parts[2]}\"}" 1>&3
|
||||
break
|
||||
done < /dev/stdin
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"createdAt": "-140h",
|
||||
"email": "alice@example.com",
|
||||
"tel": "+17025550001",
|
||||
"passhash": "alice123",
|
||||
"private": {"comment": "some comment 123"},
|
||||
"public": {"fn": "Alice Hatter", "photo": "alice-128.jpg", "type": "jpg"},
|
||||
"trusted": {"verified": true},
|
||||
"tags": ["Alice"],
|
||||
"state": "ok",
|
||||
"status": {
|
||||
"text": "DND"
|
||||
},
|
||||
"username": "alice",
|
||||
"addressBook": ["email:bob@example.com", "email:carol@example.com", "email:dave@example.com",
|
||||
"email:eve@example.com","email:frank@example.com","email:george@example.com","email:tino@example.com",
|
||||
"tel:+17025550001", "tel:+17025550002", "tel:+17025550003", "tel:+17025550004", "tel:+17025550005",
|
||||
"tel:+17025550006", "tel:+17025550007", "tel:+17025550008", "tel:+17025550009"]
|
||||
},
|
||||
{
|
||||
"createdAt": "-139h",
|
||||
"email": "bob@example.com",
|
||||
"tel": "+17025550002",
|
||||
"passhash": "bob123",
|
||||
"private": {"comment": "no comments :)"},
|
||||
"public": {"fn": "Bob Smith", "photo": "bob-128.jpg", "type": "jpg"},
|
||||
"state": "ok",
|
||||
"status": "stuff",
|
||||
"username": "bob",
|
||||
"addressBook": ["email:alice@example.com", "email:carol@example.com", "email:dave@example.com",
|
||||
"email:eve@example.com", "email:frank@example.com", "email:george@example.com", "email:tino@example.com",
|
||||
"tel:+17025550001", "tel:+17025550002", "tel:+17025550003", "tel:+17025550004", "tel:+17025550005",
|
||||
"tel:+17025550006", "tel:+17025550007", "tel:+17025550008", "tel:+17025550009"]
|
||||
},
|
||||
{
|
||||
"createdAt": "-138h",
|
||||
"email": "carol@example.com",
|
||||
"tel": "+17025550003",
|
||||
"passhash": "carol123",
|
||||
"private": {"comment": "more stuff"},
|
||||
"public": {"fn": "Carol Xmas", "photo": "carol-128.jpg", "type": "jpg"},
|
||||
"state": "",
|
||||
"status": "ho ho ho",
|
||||
"username": "carol",
|
||||
"addressBook": ["email:alice@example.com", "email:bob@example.com", "email:dave@example.com",
|
||||
"email:eve@example.com", "email:frank@example.com", "email:george@example.com", "email:tino@example.com",
|
||||
"tel:+17025550001", "tel:+17025550002", "tel:+17025550003", "tel:+17025550004", "tel:+17025550005",
|
||||
"tel:+17025550006", "tel:+17025550007", "tel:+17025550008", "tel:+17025550009"]
|
||||
},
|
||||
{
|
||||
"createdAt": "-137h",
|
||||
"email": "dave@example.com",
|
||||
"tel": "+17025550004",
|
||||
"passhash": "dave123",
|
||||
"private": {"comment": "stuff 123"},
|
||||
"public": {"fn": "Dave Goliathsson", "photo": "dave-128.jpg", "type": "jpg"},
|
||||
"state": "ok",
|
||||
"status": "hiding!",
|
||||
"username": "dave",
|
||||
"addressBook": ["email:alice@example.com", "email:bob@example.com", "email:carol@example.com",
|
||||
"email:eve@example.com", "email:frank@example.com", "email:george@example.com", "email:tino@example.com",
|
||||
"tel:+17025550001", "tel:+17025550002", "tel:+17025550003", "tel:+17025550004", "tel:+17025550005",
|
||||
"tel:+17025550006", "tel:+17025550007", "tel:+17025550008", "tel:+17025550009"]
|
||||
},
|
||||
{
|
||||
"createdAt": "-136h",
|
||||
"email": "eve@example.com",
|
||||
"tel": "+17025550005",
|
||||
"passhash": "eve123",
|
||||
"private": {"comment": "apples?"},
|
||||
"public": {"fn": "Eve Adams", "photo": "eve-128.jpg", "type": "jpg"},
|
||||
"state": "ok",
|
||||
"username": "eve",
|
||||
"addressBook": ["email:alice@example.com", "email:bob@example.com", "email:carol@example.com",
|
||||
"email:dave@example.com", "email:george@example.com", "email:tino@example.com",
|
||||
"tel:+17025550001", "tel:+17025550002", "tel:+17025550003", "tel:+17025550004", "tel:+17025550005",
|
||||
"tel:+17025550006", "tel:+17025550007", "tel:+17025550008", "tel:+17025550009"]
|
||||
},
|
||||
{
|
||||
"createdAt": "-135h",
|
||||
"email": "frank@example.com",
|
||||
"tel": "+17025550006",
|
||||
"passhash": "frank123",
|
||||
"private": {"comment": "things, not stuff"},
|
||||
"public": {"fn": "Frank Singer"},
|
||||
"state": "ok",
|
||||
"status": "singing!",
|
||||
"username": "frank",
|
||||
"addressBook": ["email:bob@example.com", "email:carol@example.com", "email:dave@example.com",
|
||||
"email:eve@example.com", "email:george@example.com", "email:tino@example.com",
|
||||
"tel:+17025550001", "tel:+17025550002", "tel:+17025550003", "tel:+17025550004", "tel:+17025550005",
|
||||
"tel:+17025550007", "tel:+17025550008", "tel:+17025550009"]
|
||||
},
|
||||
{
|
||||
"createdAt": "-134h",
|
||||
"email": "tino@example.com",
|
||||
"tel": "+17025550010",
|
||||
"passhash": "(random)",
|
||||
"private": {"comment": "I'm a chatbot short and stout"},
|
||||
"trusted": {"staff": true},
|
||||
"public": {"fn": "Tino the Chatbot", "photo": "tino-128.jpg", "type": "jpg"},
|
||||
"state": "",
|
||||
"status": "Chatting nonsensically",
|
||||
"username": "tino",
|
||||
"addressBook": []
|
||||
},
|
||||
{
|
||||
"createdAt": "-133h",
|
||||
"email": "xena@example.com",
|
||||
"tel": "+17025550099",
|
||||
"passhash": "xena123",
|
||||
"authLevel": "root",
|
||||
"private": {"comment": "No one knowns that Xena exists"},
|
||||
"public": {"fn": "Xena Pacifist Peasant", "photo": "xena-128.jpg", "type": "jpg"},
|
||||
"trusted": {"staff": true},
|
||||
"state": "",
|
||||
"status": "Just a simple peaceful agriculture specialist",
|
||||
"username": "xena",
|
||||
"addressBook": []
|
||||
}
|
||||
],
|
||||
|
||||
"grouptopics": [
|
||||
{
|
||||
"createdAt": "-128h",
|
||||
"name": "*ABC",
|
||||
"owner": "carol",
|
||||
"tags": ["flower", "flowers", "flora"],
|
||||
"public": {"fn": "Let's talk about flowers", "photo": "abc-128.jpg", "type": "jpg"}
|
||||
},
|
||||
{
|
||||
"createdAt": "-126h",
|
||||
"name": "*ABCDEF",
|
||||
"owner": "alice",
|
||||
"tags": ["travel"],
|
||||
"public": {"fn": "Travel, travel, travel", "photo": "abcdef-128.jpg", "type": "jpg"}
|
||||
},
|
||||
{
|
||||
"createdAt": "-124h",
|
||||
"name": "*BF",
|
||||
"owner": "frank",
|
||||
"tags": ["sikrit", "secret"],
|
||||
"public": {"fn": "Sikrit group!", "photo": "bf-128.jpg", "type": "jpg"}
|
||||
},
|
||||
{
|
||||
"createdAt": "-123h",
|
||||
"name": "*X",
|
||||
"owner": "xena",
|
||||
"tags": ["support","public"],
|
||||
"public": {"fn": "Support", "photo": "support-128.jpg", "type": "jpg"},
|
||||
"trusted": {"staff": true},
|
||||
"access": {"auth": "JRWP", "anon": "JW"}
|
||||
},
|
||||
{
|
||||
"createdAt": "-122h",
|
||||
"name": "*BACDF",
|
||||
"owner": "bob",
|
||||
"channel": true,
|
||||
"tags": ["coffee","channel"],
|
||||
"public": {"fn": "Coffee Channel", "photo": "chan-128.jpg", "type": "jpg"},
|
||||
"trusted": {"verified": true},
|
||||
"access": {"auth": "RWPD", "anon": "N"}
|
||||
}
|
||||
],
|
||||
"p2psubs": [
|
||||
{
|
||||
"createdAt": "-120h",
|
||||
"users": [{"name": "alice"}, {"name": "bob", "private": {"comment": "Alice Jo"}}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-119h",
|
||||
"users": [{"name": "alice"}, {"name": "carol", "private": {"comment": "Alice Joha"}}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-118h",
|
||||
"users": [{"name": "alice"}, {"name": "dave", "private": {"comment": "Alice not in Wunderland"}}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-117h",
|
||||
"private": {"comment": "apples to oranges"},
|
||||
"users": [{"name": "alice", "private": {"comment": "Apples"}},
|
||||
{"name": "eve", "private": {"comment": "Alice is not what she seems"}}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-116h",
|
||||
"users": [{"name": "alice", "private": {"comment": "Frank Frank Frank a-\u003ef"}},
|
||||
{"name": "frank", "private": {"comment": "Johnson f-\u003ea"}}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-115.5h",
|
||||
"users": [{"name": "alice", "private": {"comment": "Python chatbot, short and stout"}}, {"name": "tino"}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-114h",
|
||||
"users": [{"name": "bob", "private": {"comment": "I'm banned by Dave!"}, "have": "A"},
|
||||
{"name": "dave", "private": {"comment": "I banned Bob."}, "want": "N"}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-113.5h",
|
||||
"users": [{"name": "bob", "private": {"comment": "Eve nee Ng"}}, {"name": "eve"}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-113.45h",
|
||||
"users": [{"name": "bob", "private": {"comment": "Python chatbot, short and stout"}}, {"name": "tino"}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-113.3h",
|
||||
"users": [{"name": "carol", "private": {"comment": "Python chatbot, short and stout"}}, {"name": "tino"}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-112.7h",
|
||||
"private": {"comment": "Python chatbot short and stout"},
|
||||
"users": [{"name": "dave", "private": {"comment": "Python chatbot, short and stout"}}, {"name": "tino"}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-112.3h",
|
||||
"users": [{"name": "eve", "private": {"comment": "Python chatbot, short and stout"}}, {"name": "tino"}]
|
||||
},
|
||||
{
|
||||
"createdAt": "-112.1h",
|
||||
"users": [{"name": "frank", "private": {"comment": "Python chatbot, short and stout"}}, {"name": "tino"}]
|
||||
}
|
||||
],
|
||||
"groupsubs": [{
|
||||
"createdAt": "-112h",
|
||||
"private": {"comment": "My super cool group topic"},
|
||||
"topic": "*ABC",
|
||||
"user": "alice"
|
||||
},
|
||||
{
|
||||
"createdAt": "-111.9h",
|
||||
"private": {"comment": "Wow"},
|
||||
"topic": "*ABC",
|
||||
"user": "bob"
|
||||
},
|
||||
{
|
||||
"createdAt": "-111.8h",
|
||||
"private": {"comment": "Custom group description by Bob"},
|
||||
"topic": "*ABCDEF",
|
||||
"user": "bob"
|
||||
},
|
||||
{
|
||||
"createdAt": "-111.7h",
|
||||
"private": {"comment": "Kirgudu"},
|
||||
"topic": "*ABCDEF",
|
||||
"user": "carol"
|
||||
},
|
||||
{
|
||||
"createdAt": "-111.6h",
|
||||
"topic": "*ABCDEF",
|
||||
"user": "dave"
|
||||
},
|
||||
{
|
||||
"createdAt": "-111.5h",
|
||||
"topic": "*ABCDEF",
|
||||
"user": "eve"
|
||||
},
|
||||
{
|
||||
"createdAt": "-111.4h",
|
||||
"topic": "*ABCDEF",
|
||||
"user": "frank"
|
||||
},
|
||||
{
|
||||
"createdAt": "-111.3h",
|
||||
"private": {"comment": "I'm not the owner, Frank is"},
|
||||
"topic": "*BF",
|
||||
"user": "bob"
|
||||
},
|
||||
{
|
||||
"createdAt": "-111.2h",
|
||||
"topic": "*BACDF",
|
||||
"user": "alice"
|
||||
},
|
||||
{
|
||||
"createdAt": "-111.1h",
|
||||
"topic": "*BACDF",
|
||||
"asChan": true,
|
||||
"user": "carol"
|
||||
},
|
||||
{
|
||||
"createdAt": "-111.0h",
|
||||
"topic": "*BACDF",
|
||||
"asChan": true,
|
||||
"user": "dave"
|
||||
},
|
||||
{
|
||||
"createdAt": "-110.8h",
|
||||
"topic": "*BACDF",
|
||||
"asChan": true,
|
||||
"user": "frank"
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
"Caution: Do not view laser light with remaining eye.",
|
||||
"Caution: breathing may be hazardous to your health.",
|
||||
"Celebrate Hannibal Day this year. Take an elephant to lunch.",
|
||||
"Celibacy is not hereditary.",
|
||||
"Center 1127 -- It's not just a job, it's an adventure!",
|
||||
"Center meeting at 4pm in 2C-543",
|
||||
"Centran manuals are available in 2B-515.",
|
||||
"Charlie don't surf.",
|
||||
"Children are hereditary: if your parents didn't have any, neither will you.",
|
||||
"Clothes make the man. Naked people have little or no influence on society.",
|
||||
"Club sandwiches, not baby seals.",
|
||||
"Cocaine is nature's way of saying you make too much money.",
|
||||
"Cogito Ergo Spud.",
|
||||
"Cogito cogito ergo cogito sum.",
|
||||
"Colorless green ideas sleep furiously.",
|
||||
"Communication is only possible between equals.",
|
||||
"Computers are not intelligent. They only think they are.",
|
||||
"Consistency is always easier to defend than correctness.",
|
||||
"Constants aren't. Variables don't. LISP does. Functions won't. Bytes do.",
|
||||
"Contains no kung fu, car chases or decapitations.",
|
||||
"Continental Life. Why do you ask?",
|
||||
"Convictions cause convicts -- what you believe imprisons you.",
|
||||
"Core Error - Bus Dumped",
|
||||
"Could not open 2147478952 framebuffers.",
|
||||
"Courage is something you can never totally own or totally lose.",
|
||||
"Cowards die many times before their deaths;/The valiant never taste of death but once.",
|
||||
"Crazee Edeee, his prices are INSANE!!!",
|
||||
"Creativity is no substitute for knowing what you are doing.",
|
||||
"Creditors have much better memories than debtors.",
|
||||
"Critics are like eunuchs in a harem: they know how it's done, they've seen it done",
|
||||
"every day, but they're unable to do it themselves. -Brendan Behan",
|
||||
"Cthulhu Saves! ... in case He's hungry later.",
|
||||
"Dames is like streetcars -- The oceans is full of 'em. -Archie Bunker",
|
||||
"Dames lie about anything - just for practice. -Raymond Chandler",
|
||||
"Damn it, i gotta get outta here!",
|
||||
"Dangerous knowledge is a little thing.",
|
||||
"It is certain",
|
||||
"It is decidedly so",
|
||||
"Without a doubt",
|
||||
"Yes definitely",
|
||||
"You may rely on it",
|
||||
"As I see it yes",
|
||||
"Most likely",
|
||||
"Outlook good",
|
||||
"Yes",
|
||||
"No",
|
||||
"No! No, no, no! No!!",
|
||||
"Signs point to yes",
|
||||
"Reply hazy try again",
|
||||
"Ask again later",
|
||||
"Better not tell you now",
|
||||
"Cannot predict now",
|
||||
"Concentrate and ask again",
|
||||
"Don't count on it",
|
||||
"My reply is no",
|
||||
"My sources say no.",
|
||||
"Outlook not so good",
|
||||
"Very doubtful...",
|
||||
"All your base are belong to us."
|
||||
],
|
||||
"forms": [
|
||||
{
|
||||
"txt": "Post responseYesДа",
|
||||
"fmt": [
|
||||
{"at": 0, "len": 18, "tp": "FM"},
|
||||
{"at": 0, "len": 13, "tp": "ST"},
|
||||
{"at": 13, "len": 3, "key": 0},
|
||||
{"at": 16, "len": 2, "key": 1}
|
||||
],
|
||||
"ent": [
|
||||
{"tp": "BN", "data": {"name": "yes", "act": "pub"}},
|
||||
{"tp": "BN", "data": {"name": "duh", "val": "42", "act": "pub"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"txt": "Go to URL:Yes OK",
|
||||
"fmt": [
|
||||
{"at": 0, "len": 16, "tp": "FM"},
|
||||
{"at": 0, "len": 10, "tp": "ST"},
|
||||
{"at": 10, "len": 6, "tp": "RW"},
|
||||
{"at": 10, "len": 3, "key": 0},
|
||||
{"at": 14, "len": 2, "key": 1}
|
||||
],
|
||||
"ent": [
|
||||
{"tp": "BN", "data": {"name": "ok", "act": "url", "ref": "https://github.com/tinode/chat/?key=val"}},
|
||||
{"tp": "BN", "data": {"name": "oops", "val": "test", "act": "url", "ref": "https://github.com/tinode/chat"}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,465 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/auth"
|
||||
_ "github.com/tinode/chat/server/auth/basic"
|
||||
"github.com/tinode/chat/server/store"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
func genDb(data *Data, p2pDel bool, tenantID types.TenantID) {
|
||||
var err error
|
||||
var botAccount string
|
||||
|
||||
if len(data.Users) == 0 {
|
||||
log.Println("No data provided, stopping")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Add authentication record
|
||||
authHandler := store.Store.GetAuthHandler("basic")
|
||||
authHandler.Init([]byte(`{"add_to_tags": true}`), "basic")
|
||||
|
||||
nameIndex := make(map[string]string, len(data.Users))
|
||||
|
||||
log.Println("Generating users...")
|
||||
|
||||
for _, uu := range data.Users {
|
||||
state, err := types.NewObjState(uu.State)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
user := types.User{
|
||||
TenantID: tenantID,
|
||||
State: state,
|
||||
Access: types.DefaultAccess{
|
||||
Auth: types.ModeCAuth,
|
||||
Anon: types.ModeNone,
|
||||
},
|
||||
Tags: uu.Tags,
|
||||
Public: parsePublic(&uu.Public, data.datapath),
|
||||
}
|
||||
if !uu.Trusted.IsZero() {
|
||||
user.Trusted = uu.Trusted
|
||||
}
|
||||
user.CreatedAt = getCreatedTime(uu.CreatedAt)
|
||||
|
||||
user.Tags = append(user.Tags, "basic:"+uu.Username)
|
||||
if uu.Email != "" {
|
||||
user.Tags = append(user.Tags, "email:"+uu.Email)
|
||||
}
|
||||
if uu.Tel != "" {
|
||||
user.Tags = append(user.Tags, "tel:"+uu.Tel)
|
||||
}
|
||||
|
||||
// store.Users.Create will subscribe user to !me topic but won't create a !me topic
|
||||
if _, err := store.Users.Create(tenantID, &user, uu.Private); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Save credentials: email and phone number as if they were confirmed.
|
||||
if uu.Email != "" {
|
||||
if _, err := store.Users.UpsertCred(tenantID, &types.Credential{
|
||||
User: user.Id,
|
||||
Method: "email",
|
||||
Value: uu.Email,
|
||||
Done: true,
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if uu.Tel != "" {
|
||||
if _, err := store.Users.UpsertCred(tenantID, &types.Credential{
|
||||
User: user.Id,
|
||||
Method: "tel",
|
||||
Value: uu.Tel,
|
||||
Done: true,
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
authLevel := auth.LevelAuth
|
||||
if uu.AuthLevel != "" {
|
||||
authLevel = auth.ParseAuthLevel(uu.AuthLevel)
|
||||
if authLevel == auth.LevelNone {
|
||||
log.Fatal("Unknown authLevel", uu.AuthLevel)
|
||||
}
|
||||
}
|
||||
// Add authentication record
|
||||
authHandler := store.Store.GetAuthHandler("basic")
|
||||
passwd := uu.Password
|
||||
if passwd == "(random)" {
|
||||
// Generate random password
|
||||
passwd = getPassword(8)
|
||||
botAccount = uu.Username
|
||||
}
|
||||
if _, err := authHandler.AddRecord(auth.AuthContext{TenantID: tenantID},
|
||||
&auth.Rec{TenantID: tenantID, Uid: user.Uid(), AuthLevel: authLevel},
|
||||
[]byte(uu.Username+":"+passwd)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
nameIndex[uu.Username] = user.Id
|
||||
|
||||
// Add address book as fnd.private
|
||||
if len(uu.AddressBook) > 0 {
|
||||
if err := store.Subs.Update(tenantID, user.Uid().FndName(), user.Uid(),
|
||||
map[string]any{"Private": strings.Join(uu.AddressBook, ",")}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("usr;" + uu.Username + ";" + user.Uid().UserId() + ";" + passwd)
|
||||
}
|
||||
|
||||
if botAccount == "" && len(data.Users) > 0 {
|
||||
botAccount = data.Users[0].Username
|
||||
}
|
||||
|
||||
log.Println("Generating group topics...")
|
||||
|
||||
for _, gt := range data.Grouptopics {
|
||||
name := genTopicName()
|
||||
nameIndex[gt.Name] = name
|
||||
|
||||
accessAuth := types.ModeCPublic
|
||||
if gt.Access.Auth != "" {
|
||||
if err := accessAuth.UnmarshalText([]byte(gt.Access.Auth)); err != nil {
|
||||
log.Fatal("Invalid Auth access mode", gt.Access.Auth, err)
|
||||
}
|
||||
}
|
||||
accessAnon := types.ModeCReadOnly
|
||||
if gt.Access.Anon != "" {
|
||||
if err := accessAnon.UnmarshalText([]byte(gt.Access.Anon)); err != nil {
|
||||
log.Fatal("Invalid Anon access mode", gt.Access.Anon, err)
|
||||
}
|
||||
}
|
||||
topic := &types.Topic{
|
||||
ObjHeader: types.ObjHeader{Id: name},
|
||||
TenantID: tenantID,
|
||||
Access: types.DefaultAccess{
|
||||
Auth: accessAuth,
|
||||
Anon: accessAnon,
|
||||
},
|
||||
UseBt: gt.Channel,
|
||||
Tags: gt.Tags,
|
||||
Public: parsePublic(>.Public, data.datapath),
|
||||
}
|
||||
if !gt.Trusted.IsZero() {
|
||||
topic.Trusted = gt.Trusted
|
||||
}
|
||||
var owner types.Uid
|
||||
if gt.Owner != "" {
|
||||
owner = types.ParseUid(nameIndex[gt.Owner])
|
||||
if owner.IsZero() {
|
||||
log.Fatal("Invalid owner", gt.Owner, "for topic", gt.Name)
|
||||
}
|
||||
topic.GiveAccess(owner, types.ModeCFull, types.ModeCFull)
|
||||
}
|
||||
topic.CreatedAt = getCreatedTime(gt.CreatedAt)
|
||||
|
||||
if err = store.Topics.Create(tenantID, topic, owner, gt.OwnerPrivate); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("grp;" + gt.Name + ";" + name)
|
||||
}
|
||||
|
||||
log.Println("Generating P2P subscriptions...")
|
||||
|
||||
for i, ss := range data.P2psubs {
|
||||
if ss.Users[0].Name < ss.Users[1].Name {
|
||||
ss.pair = ss.Users[0].Name + ":" + ss.Users[1].Name
|
||||
} else {
|
||||
ss.pair = ss.Users[1].Name + ":" + ss.Users[0].Name
|
||||
}
|
||||
|
||||
uid1 := types.ParseUid(nameIndex[ss.Users[0].Name])
|
||||
uid2 := types.ParseUid(nameIndex[ss.Users[1].Name])
|
||||
topic := uid1.P2PName(uid2)
|
||||
created := getCreatedTime(ss.CreatedAt)
|
||||
|
||||
// Assign default access mode
|
||||
defaultMode := types.ModeCP2P
|
||||
if p2pDel {
|
||||
defaultMode = types.ModeCP2PD
|
||||
}
|
||||
s0want := defaultMode
|
||||
s0given := defaultMode
|
||||
s1want := defaultMode
|
||||
s1given := defaultMode
|
||||
|
||||
// Check of non-default access mode was provided
|
||||
if ss.Users[0].Want != "" {
|
||||
if err := s0want.UnmarshalText([]byte(ss.Users[0].Want)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if ss.Users[0].Have != "" {
|
||||
if err := s0given.UnmarshalText([]byte(ss.Users[0].Have)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if ss.Users[1].Want != "" {
|
||||
if err := s1want.UnmarshalText([]byte(ss.Users[1].Want)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if ss.Users[1].Have != "" {
|
||||
if err := s1given.UnmarshalText([]byte(ss.Users[1].Have)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
err := store.Topics.CreateP2P(tenantID,
|
||||
&types.Subscription{
|
||||
ObjHeader: types.ObjHeader{CreatedAt: created},
|
||||
TenantID: tenantID,
|
||||
User: uid1.String(),
|
||||
Topic: topic,
|
||||
ModeWant: s0want,
|
||||
ModeGiven: s0given,
|
||||
Private: ss.Users[0].Private},
|
||||
&types.Subscription{
|
||||
ObjHeader: types.ObjHeader{CreatedAt: created},
|
||||
TenantID: tenantID,
|
||||
User: uid2.String(),
|
||||
Topic: topic,
|
||||
ModeWant: s1want,
|
||||
ModeGiven: s1given,
|
||||
Private: ss.Users[1].Private})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
data.P2psubs[i].pair = ss.pair
|
||||
nameIndex[ss.pair] = topic
|
||||
fmt.Println("p2p;" + ss.pair + ";" + topic)
|
||||
}
|
||||
|
||||
log.Println("Generating group subscriptions...")
|
||||
|
||||
for _, ss := range data.Groupsubs {
|
||||
var want, given types.AccessMode
|
||||
if ss.AsChan {
|
||||
want = types.ModeCChnReader
|
||||
given = types.ModeCChnReader
|
||||
} else {
|
||||
want = types.ModeCPublic
|
||||
given = types.ModeCPublic
|
||||
}
|
||||
if ss.Want != "" {
|
||||
if err := want.UnmarshalText([]byte(ss.Want)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if ss.Have != "" {
|
||||
if err := given.UnmarshalText([]byte(ss.Have)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
tname := nameIndex[ss.Topic]
|
||||
if ss.AsChan {
|
||||
tname = types.GrpToChn(tname)
|
||||
}
|
||||
if err = store.Subs.Create(tenantID, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{CreatedAt: getCreatedTime(ss.CreatedAt)},
|
||||
TenantID: tenantID,
|
||||
User: nameIndex[ss.User],
|
||||
Topic: tname,
|
||||
ModeWant: want,
|
||||
ModeGiven: given,
|
||||
Private: ss.Private}); err != nil {
|
||||
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
seqIds := map[string]int{}
|
||||
now := types.TimeNow().Add(-time.Minute * 10)
|
||||
|
||||
messageCount := len(data.Messages)
|
||||
if messageCount > 0 {
|
||||
log.Println("Inserting messages...")
|
||||
|
||||
if messageCount > 1 {
|
||||
// Shuffle messages
|
||||
rand.Shuffle(len(data.Messages), func(i, j int) {
|
||||
data.Messages[i], data.Messages[j] = data.Messages[j], data.Messages[i]
|
||||
})
|
||||
|
||||
// Starting 4 days ago.
|
||||
timestamp := now.Add(time.Hour * time.Duration(-24*4))
|
||||
toInsert := 96 // 96 is the maximum, otherwise messages may appear in the future
|
||||
// Initial maximum increment of the message sent time in milliseconds
|
||||
increment := 3600 * 1000
|
||||
subIdx := rand.Intn(len(data.Groupsubs) + len(data.P2psubs)*2)
|
||||
for i := range toInsert {
|
||||
// At least 20% of subsequent messages should come from the same user in the same topic.
|
||||
if rand.Intn(5) > 0 {
|
||||
subIdx = rand.Intn(len(data.Groupsubs) + len(data.P2psubs)*2)
|
||||
}
|
||||
|
||||
var topic string
|
||||
var from types.Uid
|
||||
if subIdx < len(data.Groupsubs) {
|
||||
if data.Groupsubs[subIdx].AsChan {
|
||||
// Channel readers should not have any published messages.
|
||||
continue
|
||||
}
|
||||
topic = nameIndex[data.Groupsubs[subIdx].Topic]
|
||||
from = types.ParseUid(nameIndex[data.Groupsubs[subIdx].User])
|
||||
} else {
|
||||
idx := (subIdx - len(data.Groupsubs)) / 2
|
||||
usr := (subIdx - len(data.Groupsubs)) % 2
|
||||
sub := data.P2psubs[idx]
|
||||
topic = nameIndex[sub.pair]
|
||||
from = types.ParseUid(nameIndex[sub.Users[usr].Name])
|
||||
}
|
||||
|
||||
seqIds[topic]++
|
||||
seqId := seqIds[topic]
|
||||
str := data.Messages[i%len(data.Messages)]
|
||||
// Max time between messages is 2 hours, averate - 1 hour, time is increasing as seqId increases
|
||||
timestamp = timestamp.Add(time.Microsecond * time.Duration(rand.Intn(increment)))
|
||||
if timestamp.After(now) {
|
||||
now = timestamp
|
||||
}
|
||||
if err, _ = store.Messages.Save(tenantID, &types.Message{
|
||||
ObjHeader: types.ObjHeader{CreatedAt: timestamp},
|
||||
TenantID: tenantID,
|
||||
SeqId: seqId,
|
||||
Topic: topic,
|
||||
From: from.String(),
|
||||
Content: str,
|
||||
}, nil, true); err != nil {
|
||||
log.Fatal("Failed to insert message: ", err)
|
||||
}
|
||||
|
||||
// New increment: remaining time until 'now' divided by the number of messages to be inserted,
|
||||
// then converted to milliseconds.
|
||||
increment = int(now.Sub(timestamp).Nanoseconds() / int64(toInsert-i) / 1000000)
|
||||
|
||||
// log.Printf("Msg.seq=%d at %v, topic='%s' from='%s'", msg.SeqId, msg.CreatedAt, topic, from.UserId())
|
||||
}
|
||||
} else {
|
||||
// Only one message is provided. Just insert it into every topic.
|
||||
now := time.Now().UTC().Add(-time.Minute).Round(time.Millisecond)
|
||||
|
||||
for _, gt := range data.Grouptopics {
|
||||
seqIds[nameIndex[gt.Name]] = 1
|
||||
if err, _ = store.Messages.Save(tenantID, &types.Message{
|
||||
ObjHeader: types.ObjHeader{CreatedAt: now},
|
||||
TenantID: tenantID,
|
||||
SeqId: 1,
|
||||
Topic: nameIndex[gt.Name],
|
||||
From: nameIndex[gt.Owner],
|
||||
Content: data.Messages[0],
|
||||
}, nil, true); err != nil {
|
||||
log.Fatal("Failed to insert message: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
usedp2p := make(map[string]bool)
|
||||
for i := 0; len(usedp2p) < len(data.P2psubs)/2; i++ {
|
||||
sub := data.P2psubs[i]
|
||||
if usedp2p[nameIndex[sub.pair]] {
|
||||
continue
|
||||
}
|
||||
usedp2p[nameIndex[sub.pair]] = true
|
||||
seqIds[nameIndex[sub.pair]] = 1
|
||||
if err, _ = store.Messages.Save(tenantID, &types.Message{
|
||||
ObjHeader: types.ObjHeader{CreatedAt: now},
|
||||
TenantID: tenantID,
|
||||
SeqId: 1,
|
||||
Topic: nameIndex[sub.pair],
|
||||
From: nameIndex[sub.Users[0].Name],
|
||||
Content: data.Messages[0],
|
||||
}, nil, true); err != nil {
|
||||
log.Fatal("Failed to insert message: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(data.Forms) != 0 {
|
||||
from := nameIndex[botAccount]
|
||||
log.Println("Inserting forms as ", botAccount, from)
|
||||
ts := now
|
||||
for _, form := range data.Forms {
|
||||
for _, sub := range data.P2psubs {
|
||||
ts = ts.Add(time.Second)
|
||||
seqIds[nameIndex[sub.pair]]++
|
||||
seqId := seqIds[nameIndex[sub.pair]]
|
||||
if sub.Users[0].Name == botAccount || sub.Users[1].Name == botAccount {
|
||||
if err, _ = store.Messages.Save(tenantID, &types.Message{
|
||||
ObjHeader: types.ObjHeader{CreatedAt: ts},
|
||||
TenantID: tenantID,
|
||||
SeqId: seqId,
|
||||
Topic: nameIndex[sub.pair],
|
||||
Head: types.KVMap{"mime": "text/x-drafty"},
|
||||
From: from,
|
||||
Content: form,
|
||||
}, nil, true); err != nil {
|
||||
log.Fatal("Failed to insert form: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Sample data processing completed.")
|
||||
}
|
||||
|
||||
// Go json cannot unmarshal Duration from a string, thus this hack.
|
||||
func getCreatedTime(delta string) time.Time {
|
||||
dd, err := time.ParseDuration(delta)
|
||||
if err != nil && delta != "" {
|
||||
log.Fatal("Invalid duration string", delta)
|
||||
}
|
||||
|
||||
return time.Now().UTC().Round(time.Millisecond).Add(dd)
|
||||
}
|
||||
|
||||
type photoStruct struct {
|
||||
Type string `json:"type" db:"type"`
|
||||
Data []byte `json:"data" db:"data"`
|
||||
}
|
||||
|
||||
type card struct {
|
||||
Fn string `json:"fn" db:"fn"`
|
||||
Photo *photoStruct `json:"photo,omitempty" db:"photo"`
|
||||
}
|
||||
|
||||
// {"fn": "Alice Johnson", "photo": "alice-128.jpg"}
|
||||
func parsePublic(public *theCard, path string) *card {
|
||||
var photo *photoStruct
|
||||
var err error
|
||||
|
||||
if public.Fn == "" && public.Photo == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fname := public.Photo; fname != "" {
|
||||
photo = &photoStruct{Type: public.Type}
|
||||
dir, _ := filepath.Split(fname)
|
||||
if dir == "" {
|
||||
dir = path
|
||||
}
|
||||
photo.Data, err = os.ReadFile(filepath.Join(dir, fname))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return &card{Fn: public.Fn, Photo: photo}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# Script that generates a synthetic Tinode dataset in the same format as data.json.
|
||||
# Run as:
|
||||
# $ python generate_dataset.py --num_users X
|
||||
#
|
||||
# User ids and passwords are: userN and userN123 where N is in [0..X).
|
||||
# The output will be dumped to stdout.
|
||||
|
||||
import argparse as ap
|
||||
import json
|
||||
import numpy as np
|
||||
import random
|
||||
import sys
|
||||
|
||||
parser = ap.ArgumentParser(description='Simple python script to generate synthetic Tinode datasets.')
|
||||
parser.add_argument('--num_users', type=int, default=50, help='Number of user accounts (default: 50).')
|
||||
args = parser.parse_args()
|
||||
|
||||
random.seed()
|
||||
np.random.seed()
|
||||
|
||||
data = dict()
|
||||
|
||||
# Users.
|
||||
num_users = args.num_users
|
||||
users = list()
|
||||
ul = ['user%d' % i for i in range(num_users)]
|
||||
|
||||
for i in range(num_users):
|
||||
userid = ul[i]
|
||||
u = {
|
||||
'createdAt': '-%dh' % random.randint(1, 300),
|
||||
'email': '%s@example.com' % userid,
|
||||
'passhash': '%s123' % userid,
|
||||
'private': {'comment': 'some comment 123'},
|
||||
'public': {'fn': userid},
|
||||
'tags': [userid],
|
||||
'state': 'ok',
|
||||
'status': {
|
||||
'text': 'my status %s' % userid
|
||||
},
|
||||
'username': userid,
|
||||
}
|
||||
users.append(u)
|
||||
|
||||
data['users'] = users
|
||||
|
||||
# Groups.
|
||||
group_topics = list()
|
||||
# TODO: make it configurable.
|
||||
num_groups = random.randint(1, num_users >> 1)
|
||||
for i in range(num_groups):
|
||||
owner = random.choice(ul)
|
||||
g = {
|
||||
'createdAt': '-%dh' % random.randint(1, 300),
|
||||
'name': '*ABCgroup%d' % i,
|
||||
'owner': owner,
|
||||
'tags': ['group%d' % i],
|
||||
'public': {'fn': 'My group %d' % i}
|
||||
}
|
||||
group_topics.append(g)
|
||||
|
||||
data['grouptopics'] = group_topics
|
||||
|
||||
# P2P subs.
|
||||
p2p_subs = list()
|
||||
ids = set()
|
||||
|
||||
# TODO: Poisson mean should be configurable.
|
||||
num_contacts = np.random.poisson(40, num_users).tolist()
|
||||
num_contacts = [min(max(2, int(s)), num_users - 1) for s in num_contacts]
|
||||
|
||||
all_ids = [i for i in range(num_users)]
|
||||
|
||||
for i in range(num_users):
|
||||
contacts = [ul[x] for x in random.sample(all_ids, num_contacts[i]) if x > i]
|
||||
for c in contacts:
|
||||
p2p = {
|
||||
'createdAt': '-%dh' % random.randint(1, 300),
|
||||
'users': [{'name': ul[i]}, {'name': c}]
|
||||
}
|
||||
p2p_subs.append(p2p)
|
||||
|
||||
data['p2psubs'] = p2p_subs
|
||||
|
||||
# Group subs.
|
||||
# TODO: Poisson mean should be configurable as well.
|
||||
sizes = np.random.poisson(4, num_groups).tolist()
|
||||
sizes = [min(max(2, int(s)), num_users) for s in sizes]
|
||||
group_subs = list()
|
||||
|
||||
for i in range(num_groups):
|
||||
k = sizes[i]
|
||||
gs = random.sample(ul, k)
|
||||
for uid in gs:
|
||||
g = {
|
||||
'createdAt': '-%dh' % random.randint(1, 300),
|
||||
'topic': '*ABCgroup%d' % i,
|
||||
'user': uid
|
||||
}
|
||||
group_subs.append(g)
|
||||
|
||||
data['groupsubs'] = group_subs
|
||||
|
||||
json.dump(data, sys.stdout, ensure_ascii=False, indent=4)
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
crand "crypto/rand"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/auth"
|
||||
_ "github.com/tinode/chat/server/db/mongodb"
|
||||
_ "github.com/tinode/chat/server/db/mysql"
|
||||
_ "github.com/tinode/chat/server/db/postgres"
|
||||
_ "github.com/tinode/chat/server/db/rethinkdb"
|
||||
"github.com/tinode/chat/server/store"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
jcr "github.com/tinode/jsonco"
|
||||
)
|
||||
|
||||
type configType struct {
|
||||
P2PDeleteEnabled bool `json:"p2p_delete_enabled"`
|
||||
StoreConfig json.RawMessage `json:"store_config"`
|
||||
}
|
||||
|
||||
type theCard struct {
|
||||
Fn string `json:"fn"`
|
||||
Photo string `json:"photo"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type tPrivate struct {
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type tTrusted struct {
|
||||
Verified bool `json:"verified,omitempty"`
|
||||
Staff bool `json:"staff,omitempty"`
|
||||
}
|
||||
|
||||
func (t tTrusted) IsZero() bool {
|
||||
return !t.Verified && !t.Staff
|
||||
}
|
||||
|
||||
// DefAccess is default access mode.
|
||||
type DefAccess struct {
|
||||
Auth string `json:"auth"`
|
||||
Anon string `json:"anon"`
|
||||
}
|
||||
|
||||
/*
|
||||
User object in data.json
|
||||
|
||||
"createdAt": "-140h",
|
||||
"email": "alice@example.com",
|
||||
"tel": "17025550001",
|
||||
"passhash": "alice123",
|
||||
"private": {"comment": "some comment 123"},
|
||||
"public": {"fn": "Alice Johnson", "photo": "alice-64.jpg", "type": "jpg"},
|
||||
"state": "ok",
|
||||
"authLevel": "auth",
|
||||
"status": {
|
||||
"text": "DND"
|
||||
},
|
||||
"username": "alice",
|
||||
"tags": ["tag1"],
|
||||
"addressBook": ["email:bob@example.com", "email:carol@example.com", "email:dave@example.com",
|
||||
"email:eve@example.com","email:frank@example.com","email:george@example.com","email:tob@example.com",
|
||||
"tel:17025550001", "tel:17025550002", "tel:17025550003", "tel:17025550004", "tel:17025550005",
|
||||
"tel:17025550006", "tel:17025550007", "tel:17025550008", "tel:17025550009"]
|
||||
}
|
||||
*/
|
||||
type User struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Email string `json:"email"`
|
||||
Tel string `json:"tel"`
|
||||
AuthLevel string `json:"authLevel"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"passhash"`
|
||||
Private tPrivate `json:"private"`
|
||||
Public theCard `json:"public"`
|
||||
Trusted tTrusted `json:"trusted"`
|
||||
State string `json:"state"`
|
||||
Status any `json:"status"`
|
||||
AddressBook []string `json:"addressBook"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
/*
|
||||
GroupTopic object in data.json
|
||||
|
||||
"createdAt": "-128h",
|
||||
"name": "*ABC",
|
||||
"owner": "carol",
|
||||
"channel": true,
|
||||
"public": {"fn": "Let's talk about flowers", "photo": "abc-64.jpg", "type": "jpg"}
|
||||
*/
|
||||
type GroupTopic struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Channel bool `json:"channel"`
|
||||
Public theCard `json:"public"`
|
||||
Trusted tTrusted `json:"trusted"`
|
||||
Access DefAccess `json:"access"`
|
||||
Tags []string `json:"tags"`
|
||||
OwnerPrivate tPrivate `json:"ownerPrivate"`
|
||||
}
|
||||
|
||||
/*
|
||||
GroupSub object in data.json
|
||||
|
||||
"createdAt": "-112h",
|
||||
"private": "My super cool group topic",
|
||||
"topic": "*ABC",
|
||||
"user": "alice",
|
||||
"asChan: false,
|
||||
"want": "JRWPSA",
|
||||
"have": "JRWP"
|
||||
*/
|
||||
type GroupSub struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Private tPrivate `json:"private"`
|
||||
Topic string `json:"topic"`
|
||||
User string `json:"user"`
|
||||
AsChan bool `json:"asChan"`
|
||||
Want string `json:"want"`
|
||||
Have string `json:"have"`
|
||||
}
|
||||
|
||||
/*
|
||||
P2PUser topic in data.json
|
||||
|
||||
"createdAt": "-117h",
|
||||
"users": [
|
||||
|
||||
{"name": "eve", "private": {"comment":"ho ho"}, "want": "JRWP", "have": "N"},
|
||||
{"name": "alice", "private": {"comment": "ha ha"}}
|
||||
|
||||
]
|
||||
*/
|
||||
type P2PUser struct {
|
||||
Name string `json:"name"`
|
||||
Private tPrivate `json:"private"`
|
||||
Want string `json:"want"`
|
||||
Have string `json:"have"`
|
||||
}
|
||||
|
||||
// P2PSub is a p2p subscription in data.json
|
||||
type P2PSub struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Users []P2PUser `json:"users"`
|
||||
// Cached value 'user1:user2' as a surrogare topic name
|
||||
pair string
|
||||
}
|
||||
|
||||
// Data is a message in data.json.
|
||||
type Data struct {
|
||||
Users []User `json:"users"`
|
||||
Grouptopics []GroupTopic `json:"grouptopics"`
|
||||
Groupsubs []GroupSub `json:"groupsubs"`
|
||||
P2psubs []P2PSub `json:"p2psubs"`
|
||||
Messages []string `json:"messages"`
|
||||
Forms []map[string]any `json:"forms"`
|
||||
datapath string
|
||||
}
|
||||
|
||||
// Generate random string as a name of the group topic
|
||||
func genTopicName() string {
|
||||
return "grp" + store.Store.GetUidString()
|
||||
}
|
||||
|
||||
// Generates password of length n
|
||||
func getPassword(n int) string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/.+?=&"
|
||||
|
||||
rbuf := make([]byte, n)
|
||||
if _, err := crand.Read(rbuf); err != nil {
|
||||
log.Fatalln("Unable to generate password", err)
|
||||
}
|
||||
|
||||
passwd := make([]byte, n)
|
||||
for i, r := range rbuf {
|
||||
passwd[i] = letters[int(r)%len(letters)]
|
||||
}
|
||||
|
||||
return string(passwd)
|
||||
}
|
||||
|
||||
func main() {
|
||||
reset := flag.Bool("reset", false, "force database reset")
|
||||
upgrade := flag.Bool("upgrade", false, "perform database version upgrade")
|
||||
noInit := flag.Bool("no_init", false, "check that database exists but don't create if missing")
|
||||
addRoot := flag.String("add_root", "", "create ROOT user, auth scheme 'basic'")
|
||||
makeRoot := flag.String("make_root", "", "promote ordinary user to ROOT, auth scheme 'basic'")
|
||||
datafile := flag.String("data", "", "name of file with sample data to load")
|
||||
conffile := flag.String("config", "./tinode.conf", "config of the database connection")
|
||||
tenantIDFlag := flag.Int64("tenant_id", 0, "tenant ID for users created or modified by this command")
|
||||
|
||||
flag.Parse()
|
||||
tenantID := types.TenantID(*tenantIDFlag)
|
||||
|
||||
var data Data
|
||||
if *datafile != "" && *datafile != "-" {
|
||||
raw, err := os.ReadFile(*datafile)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to read sample data file:", err)
|
||||
}
|
||||
err = json.Unmarshal(raw, &data)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to parse sample data:", err)
|
||||
}
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
data.datapath, _ = filepath.Split(*datafile)
|
||||
|
||||
var config configType
|
||||
if file, err := os.Open(*conffile); err != nil {
|
||||
log.Fatalln("Failed to read config file:", err)
|
||||
} else {
|
||||
jr := jcr.New(file)
|
||||
if err = json.NewDecoder(jr).Decode(&config); err != nil {
|
||||
switch jerr := err.(type) {
|
||||
case *json.UnmarshalTypeError:
|
||||
lnum, cnum, _ := jr.LineAndChar(jerr.Offset)
|
||||
log.Fatalf("Unmarshall error in config file in %s at %d:%d (offset %d bytes): %s",
|
||||
jerr.Field, lnum, cnum, jerr.Offset, jerr.Error())
|
||||
case *json.SyntaxError:
|
||||
lnum, cnum, _ := jr.LineAndChar(jerr.Offset)
|
||||
log.Fatalf("Syntax error in config file at %d:%d (offset %d bytes): %s",
|
||||
lnum, cnum, jerr.Offset, jerr.Error())
|
||||
default:
|
||||
log.Fatal("Failed to parse config file: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := store.Store.Open(1, config.StoreConfig)
|
||||
defer store.Store.Close()
|
||||
|
||||
adapterVersion := store.Store.GetAdapterVersion()
|
||||
databaseVersion := 0
|
||||
if store.Store.IsOpen() {
|
||||
databaseVersion = store.Store.GetDbVersion()
|
||||
}
|
||||
log.Printf("Database adapter: '%s'; version: %d", store.Store.GetAdapterName(), adapterVersion)
|
||||
|
||||
var created bool
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Database not initialized") {
|
||||
if *noInit {
|
||||
log.Fatalln("Database not found.")
|
||||
}
|
||||
log.Println("Database not found. Creating.")
|
||||
err = store.Store.InitDb(config.StoreConfig, false)
|
||||
if err == nil {
|
||||
log.Println("Database successfully created.")
|
||||
created = true
|
||||
}
|
||||
} else if strings.Contains(err.Error(), "Invalid database version") {
|
||||
msg := "Wrong DB version: expected " + strconv.Itoa(adapterVersion) + ", got " +
|
||||
strconv.Itoa(databaseVersion) + "."
|
||||
|
||||
if *reset {
|
||||
log.Println(msg, "Reset Requested. Dropping and recreating the database.")
|
||||
err = store.Store.InitDb(config.StoreConfig, true)
|
||||
if err == nil {
|
||||
log.Println("Database successfully reset.")
|
||||
}
|
||||
} else if *upgrade {
|
||||
if databaseVersion > adapterVersion {
|
||||
log.Fatalln(msg, "Unable to upgrade: database has greater version than the adapter.")
|
||||
}
|
||||
log.Println(msg, "Upgrading the database.")
|
||||
err = store.Store.UpgradeDb(config.StoreConfig)
|
||||
if err == nil {
|
||||
log.Println("Database successfully upgraded.")
|
||||
}
|
||||
} else {
|
||||
log.Fatalln(msg, "Use --reset to reset, --upgrade to upgrade.")
|
||||
}
|
||||
} else {
|
||||
log.Fatalln("Failed to init DB adapter:", err)
|
||||
}
|
||||
} else if *reset {
|
||||
log.Println("Reset requested. Dropping and recreating the database.")
|
||||
err = store.Store.InitDb(config.StoreConfig, true)
|
||||
if err == nil {
|
||||
log.Println("Database successfully reset.")
|
||||
}
|
||||
} else {
|
||||
log.Println("Database exists, version is correct.")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("Failure:", err)
|
||||
}
|
||||
|
||||
if *reset || created {
|
||||
if tenantID.IsZero() && len(data.Users) > 0 {
|
||||
log.Fatalln("Must specify --tenant_id when loading sample users")
|
||||
}
|
||||
genDb(&data, config.P2PDeleteEnabled, tenantID)
|
||||
} else if len(data.Users) > 0 {
|
||||
log.Println("Sample data ignored.")
|
||||
}
|
||||
|
||||
// Promote existing user account to root
|
||||
if *makeRoot != "" {
|
||||
if tenantID.IsZero() {
|
||||
log.Fatalln("Must specify --tenant_id when promoting a tenant user")
|
||||
}
|
||||
adapter := store.Store.GetAdapter()
|
||||
userId := types.ParseUserId(*makeRoot)
|
||||
if userId.IsZero() {
|
||||
log.Fatalf("Must specify a valid user ID '%s' to promote to ROOT", *makeRoot)
|
||||
}
|
||||
login, _, _, _, err := store.Users.GetAuthRecord(tenantID, userId, "basic")
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to read user authentication record", err)
|
||||
}
|
||||
tenantAdapter, ok := adapter.(interface {
|
||||
TenantAuthUpdRecord(types.TenantID, types.Uid, string, string, auth.Level, []byte, time.Time) error
|
||||
})
|
||||
if !ok {
|
||||
log.Fatalln("Configured adapter does not support tenant authentication")
|
||||
}
|
||||
if err = tenantAdapter.TenantAuthUpdRecord(tenantID, userId, "basic", "basic:"+login,
|
||||
auth.LevelRoot, nil, time.Time{}); err != nil {
|
||||
log.Fatalln("Failed to promote user to ROOT", err)
|
||||
}
|
||||
log.Printf("User '%s' promoted to ROOT", *makeRoot)
|
||||
}
|
||||
|
||||
// Create root user account.
|
||||
if *addRoot != "" {
|
||||
if tenantID.IsZero() {
|
||||
log.Fatalln("Must specify --tenant_id when creating a tenant user")
|
||||
}
|
||||
var password string
|
||||
parts := strings.Split(*addRoot, ":")
|
||||
uname := parts[0]
|
||||
if len(uname) < 3 {
|
||||
log.Fatalf("Failed to create a ROOT user: username '%s' is too short", uname)
|
||||
}
|
||||
|
||||
if len(parts) == 1 || parts[1] == "" {
|
||||
password = getPassword(10)
|
||||
} else {
|
||||
password = parts[1]
|
||||
}
|
||||
|
||||
var user types.User
|
||||
user.TenantID = tenantID
|
||||
user.Public = &card{
|
||||
Fn: "ROOT " + uname,
|
||||
}
|
||||
if _, err := store.Users.Create(tenantID, &user, nil); err != nil {
|
||||
log.Fatalln("Failed to create ROOT user:", err)
|
||||
}
|
||||
|
||||
authHandler := store.Store.GetAuthHandler("basic")
|
||||
ctx := auth.AuthContext{TenantID: tenantID}
|
||||
if _, err := authHandler.AddRecord(ctx,
|
||||
&auth.Rec{TenantID: tenantID, Uid: user.Uid(), AuthLevel: auth.LevelRoot},
|
||||
[]byte(uname+":"+password)); err != nil {
|
||||
store.Users.Delete(tenantID, user.Uid(), true)
|
||||
log.Fatalln("Failed to add ROOT auth record:", err)
|
||||
}
|
||||
log.Printf("ROOT user created: '%s:%s'", uname, password)
|
||||
}
|
||||
|
||||
log.Println("All done.")
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"p2p_delete_enabled": true,
|
||||
"store_config": {
|
||||
"uid_key": "la6YsO+bNX/+XIkOqc5Svw==",
|
||||
"use_adapter": "mysql",
|
||||
"adapters": {
|
||||
"postgres": {
|
||||
"database": "tinode",
|
||||
"dsn": "postgresql://postgres:postgres@localhost:5432/tinode?sslmode=disable"
|
||||
},
|
||||
"mysql": {
|
||||
"database": "tinode",
|
||||
"dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(120.25.72.121:53306)/tinode?parseTime=true&collation=utf8mb4_unicode_ci"
|
||||
},
|
||||
"rethinkdb": {
|
||||
"database": "tinode",
|
||||
"addresses": "localhost:28015"
|
||||
},
|
||||
"mongodb": {
|
||||
"database": "tinode",
|
||||
"addresses": "localhost:27017",
|
||||
//"replica_set": "rs0",
|
||||
//"auth_source": "admin",
|
||||
//"username": "tinode",
|
||||
//"password": "tinode",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 14 KiB |