mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 18:52:41 +01:00
Merge pull request #1 from jlatt/persistent-channels
persist channels to a sqlite db
This commit is contained in:
commit
2f149cad1d
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Jeremy Latt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -40,5 +40,6 @@ byte strings. You can generate them with e.g. `ergonomadic -genpasswd
|
||||
```sh
|
||||
go get
|
||||
go install
|
||||
ergonomadic -conf '/path/to/config.json' -initdb
|
||||
ergonomadic -conf '/path/to/config.json'
|
||||
```
|
||||
|
52
config.json
52
config.json
@ -1,13 +1,39 @@
|
||||
{ "name": "irc.example.com",
|
||||
"motd": "motd.txt",
|
||||
"listeners": [
|
||||
{ "address": "localhost:7777" },
|
||||
{ "address": "[::1]:7777" } ],
|
||||
"operators": [
|
||||
{ "name": "root",
|
||||
"password": "JDJhJDEwJFRWWGUya2E3Unk5bnZlb2o3alJ0ZnVQQm9ZVW1HOE53L29nVHg5QWh5TnpaMmtOaEwya1Vl" } ],
|
||||
"debug": {
|
||||
"net": true,
|
||||
"client": false,
|
||||
"channel": false,
|
||||
"server": false } }
|
||||
// Ergonomadic IRC Server Config
|
||||
// -----------------------------
|
||||
// Passwords are generated by `ergonomadic -genpasswd "$plaintext"`.
|
||||
// Comments are not allowed in the actual config file.
|
||||
{
|
||||
// `name` is usually a hostname.
|
||||
"name": "irc.example.com",
|
||||
|
||||
// The path to the MOTD is relative to this file's directory.
|
||||
"motd": "motd.txt",
|
||||
|
||||
// PASS command password
|
||||
"password": "JDJhJDA0JHBBenUyV3Z5UU5iWUpiYmlNMlNLZC5VRDZDM21HUzFVbmxLUUI3NTVTLkZJOERLdUFaUWNt",
|
||||
|
||||
// `listeners` are places to bind and listen for
|
||||
// connections. http://golang.org/pkg/net/#Dial demonstrates valid
|
||||
// values for `net` and `address`. `net` is optional and defaults
|
||||
// to `tcp`.
|
||||
"listeners": [ {
|
||||
"address": "localhost:7777"
|
||||
}, {
|
||||
"net": "tcp6",
|
||||
"address": "[::1]:7777"
|
||||
} ],
|
||||
|
||||
// Operators for the OPER command
|
||||
"operators": [ {
|
||||
"name": "root",
|
||||
"password": "JDJhJDA0JHBBenUyV3Z5UU5iWUpiYmlNMlNLZC5VRDZDM21HUzFVbmxLUUI3NTVTLkZJOERLdUFaUWNt"
|
||||
} ],
|
||||
|
||||
// Global debug flags. `net` generates a lot of output.
|
||||
"debug": {
|
||||
"net": true,
|
||||
"client": false,
|
||||
"channel": false,
|
||||
"server": false
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"code.google.com/p/go.crypto/bcrypt"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/jlatt/ergonomadic/irc"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func genPasswd(passwd string) {
|
||||
@ -18,8 +21,30 @@ func genPasswd(passwd string) {
|
||||
fmt.Println(encoded)
|
||||
}
|
||||
|
||||
func initDB(config *irc.Config) {
|
||||
os.Remove(config.Database())
|
||||
|
||||
db, err := sql.Open("sqlite3", config.Database())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE channel (
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
flags TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
topic TEXT NOT NULL,
|
||||
user_limit INTEGER DEFAULT 0)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
conf := flag.String("conf", "ergonomadic.json", "ergonomadic config file")
|
||||
initdb := flag.Bool("initdb", false, "initialize database")
|
||||
passwd := flag.String("genpasswd", "", "bcrypt a password")
|
||||
flag.Parse()
|
||||
|
||||
@ -31,9 +56,14 @@ func main() {
|
||||
config, err := irc.LoadConfig(*conf)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if *initdb {
|
||||
initDB(config)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO move to data structures
|
||||
irc.DEBUG_NET = config.Debug["net"]
|
||||
irc.DEBUG_CLIENT = config.Debug["client"]
|
||||
irc.DEBUG_CHANNEL = config.Debug["channel"]
|
||||
|
@ -33,7 +33,9 @@ func NewChannel(s *Server, name string) *Channel {
|
||||
name: name,
|
||||
server: s,
|
||||
}
|
||||
|
||||
s.channels[name] = channel
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
@ -142,7 +144,9 @@ func (channel *Channel) Join(client *Client, key string) {
|
||||
client.channels.Add(channel)
|
||||
channel.members.Add(client)
|
||||
if len(channel.members) == 1 {
|
||||
channel.members[client][ChannelCreator] = true
|
||||
if !channel.flags[Persistent] {
|
||||
channel.members[client][ChannelCreator] = true
|
||||
}
|
||||
channel.members[client][ChannelOperator] = true
|
||||
}
|
||||
|
||||
@ -166,7 +170,7 @@ func (channel *Channel) Part(client *Client, message string) {
|
||||
}
|
||||
channel.Quit(client)
|
||||
|
||||
if channel.IsEmpty() {
|
||||
if !channel.flags[Persistent] && channel.IsEmpty() {
|
||||
channel.server.channels.Remove(channel)
|
||||
}
|
||||
}
|
||||
@ -203,6 +207,8 @@ func (channel *Channel) SetTopic(client *Client, topic string) {
|
||||
for member := range channel.members {
|
||||
member.Reply(reply)
|
||||
}
|
||||
|
||||
channel.Persist()
|
||||
}
|
||||
|
||||
func (channel *Channel) CanSpeak(client *Client) bool {
|
||||
@ -296,7 +302,7 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo
|
||||
}
|
||||
client.RplEndOfMaskList(change.mode, channel)
|
||||
|
||||
case Moderated, NoOutside, OpOnlyTopic, Private:
|
||||
case Moderated, NoOutside, OpOnlyTopic, Persistent, Private:
|
||||
return channel.applyModeFlag(client, change.mode, change.op)
|
||||
|
||||
case Key:
|
||||
@ -361,6 +367,21 @@ func (channel *Channel) Mode(client *Client, changes ChannelModeChanges) {
|
||||
for member := range channel.members {
|
||||
member.Reply(reply)
|
||||
}
|
||||
|
||||
channel.Persist()
|
||||
}
|
||||
}
|
||||
|
||||
func (channel *Channel) Persist() {
|
||||
if channel.flags[Persistent] {
|
||||
channel.server.db.Exec(`
|
||||
INSERT OR REPLACE INTO channel
|
||||
(name, flags, key, topic)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
channel.name, channel.flags.String(), channel.key, channel.topic,
|
||||
channel.userLimit)
|
||||
} else {
|
||||
channel.server.db.Exec(`DELETE FROM channel WHERE name = ?`, channel.name)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,35 +96,17 @@ var (
|
||||
spacesExpr = regexp.MustCompile(` +`)
|
||||
)
|
||||
|
||||
func parseArg(line string) (arg string, rest string) {
|
||||
if line == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, ":") {
|
||||
arg = line[1:]
|
||||
func parseLine(line string) (StringCode, []string) {
|
||||
var parts []string
|
||||
if colonIndex := strings.IndexRune(line, ':'); colonIndex >= 0 {
|
||||
lastArg := line[colonIndex+len(":"):]
|
||||
line = line[:colonIndex-len(" ")]
|
||||
parts = append(spacesExpr.Split(line, -1), lastArg)
|
||||
} else {
|
||||
parts := spacesExpr.Split(line, 2)
|
||||
arg = parts[0]
|
||||
if len(parts) > 1 {
|
||||
rest = parts[1]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
parts = spacesExpr.Split(line, -1)
|
||||
|
||||
func parseLine(line string) (command StringCode, args []string) {
|
||||
args = make([]string, 0)
|
||||
for arg, rest := parseArg(line); arg != ""; arg, rest = parseArg(rest) {
|
||||
if arg == "" {
|
||||
continue
|
||||
}
|
||||
args = append(args, arg)
|
||||
}
|
||||
if len(args) > 0 {
|
||||
command, args = StringCode(strings.ToUpper(args[0])), args[1:]
|
||||
}
|
||||
return
|
||||
return StringCode(strings.ToUpper(parts[0])), parts[1:]
|
||||
}
|
||||
|
||||
// <command> [args...]
|
||||
|
@ -26,6 +26,11 @@ type Config struct {
|
||||
Name string
|
||||
Operators []OperatorConfig
|
||||
Password string
|
||||
directory string
|
||||
}
|
||||
|
||||
func (conf *Config) Database() string {
|
||||
return filepath.Join(conf.directory, "ergonomadic.db")
|
||||
}
|
||||
|
||||
func (conf *Config) PasswordBytes() []byte {
|
||||
@ -75,9 +80,8 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
dir := filepath.Dir(filename)
|
||||
config.MOTD = filepath.Join(dir, config.MOTD)
|
||||
|
||||
config.directory = filepath.Dir(filename)
|
||||
config.MOTD = filepath.Join(config.directory, config.MOTD)
|
||||
for _, lconf := range config.Listeners {
|
||||
if lconf.Net == "" {
|
||||
lconf.Net = "tcp"
|
||||
|
@ -23,9 +23,9 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
SERVER_VERSION = "1.1.0"
|
||||
CRLF = "\r\n"
|
||||
MAX_REPLY_LEN = 512 - len(CRLF)
|
||||
SEM_VER = "ergonomadic-1.1.0"
|
||||
CRLF = "\r\n"
|
||||
MAX_REPLY_LEN = 512 - len(CRLF)
|
||||
|
||||
LOGIN_TIMEOUT = time.Minute / 2 // how long the client has to login
|
||||
IDLE_TIMEOUT = time.Minute // how long before a client is considered idle
|
||||
@ -209,7 +209,7 @@ const (
|
||||
LocalOperator UserMode = 'O'
|
||||
Operator UserMode = 'o'
|
||||
Restricted UserMode = 'r'
|
||||
ServerNotice UserMode = 's'
|
||||
ServerNotice UserMode = 's' // deprecated
|
||||
WallOps UserMode = 'w'
|
||||
|
||||
Anonymous ChannelMode = 'a' // flag
|
||||
@ -223,6 +223,7 @@ const (
|
||||
Moderated ChannelMode = 'm' // flag
|
||||
NoOutside ChannelMode = 'n' // flag
|
||||
OpOnlyTopic ChannelMode = 't' // flag
|
||||
Persistent ChannelMode = 'P' // flag
|
||||
Private ChannelMode = 'p' // flag
|
||||
Quiet ChannelMode = 'q' // flag
|
||||
ReOp ChannelMode = 'r' // flag
|
||||
|
@ -151,7 +151,7 @@ func (target *Client) RplWelcome() {
|
||||
|
||||
func (target *Client) RplYourHost() {
|
||||
target.NumericReply(RPL_YOURHOST,
|
||||
":Your host is %s, running version %s", target.server.name, SERVER_VERSION)
|
||||
":Your host is %s, running version %s", target.server.name, SEM_VER)
|
||||
}
|
||||
|
||||
func (target *Client) RplCreated() {
|
||||
@ -161,7 +161,7 @@ func (target *Client) RplCreated() {
|
||||
|
||||
func (target *Client) RplMyInfo() {
|
||||
target.NumericReply(RPL_MYINFO,
|
||||
"%s %s aiOorsw abeIikmntpqrsl", target.server.name, SERVER_VERSION)
|
||||
"%s %s aiOorsw abeIikmntpqrsl", target.server.name, SEM_VER)
|
||||
}
|
||||
|
||||
func (target *Client) RplUModeIs(client *Client) {
|
||||
@ -371,7 +371,7 @@ func (target *Client) RplWhoisChannels(client *Client) {
|
||||
|
||||
func (target *Client) RplVersion() {
|
||||
target.NumericReply(RPL_VERSION,
|
||||
"ergonomadic-%s %s", SERVER_VERSION, target.server.name)
|
||||
"%s %s", SEM_VER, target.server.name)
|
||||
}
|
||||
|
||||
func (target *Client) RplInviting(invitee *Client, channel string) {
|
||||
|
@ -4,11 +4,14 @@ import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
@ -21,30 +24,43 @@ type Server struct {
|
||||
clients ClientNameMap
|
||||
commands chan Command
|
||||
ctime time.Time
|
||||
db *sql.DB
|
||||
idle chan *Client
|
||||
motdFile string
|
||||
name string
|
||||
newConns chan net.Conn
|
||||
operators map[string][]byte
|
||||
password []byte
|
||||
signals chan os.Signal
|
||||
timeout chan *Client
|
||||
}
|
||||
|
||||
func NewServer(config *Config) *Server {
|
||||
db, err := sql.Open("sqlite3", config.Database())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
server := &Server{
|
||||
channels: make(ChannelNameMap),
|
||||
clients: make(ClientNameMap),
|
||||
commands: make(chan Command, 16),
|
||||
ctime: time.Now(),
|
||||
db: db,
|
||||
idle: make(chan *Client, 16),
|
||||
motdFile: config.MOTD,
|
||||
name: config.Name,
|
||||
newConns: make(chan net.Conn, 16),
|
||||
operators: config.OperatorsMap(),
|
||||
password: config.PasswordBytes(),
|
||||
signals: make(chan os.Signal, 1),
|
||||
timeout: make(chan *Client, 16),
|
||||
}
|
||||
|
||||
signal.Notify(server.signals, os.Interrupt, os.Kill)
|
||||
|
||||
server.loadChannels()
|
||||
|
||||
for _, listenerConf := range config.Listeners {
|
||||
go server.listen(listenerConf)
|
||||
}
|
||||
@ -52,6 +68,32 @@ func NewServer(config *Config) *Server {
|
||||
return server
|
||||
}
|
||||
|
||||
func (server *Server) loadChannels() {
|
||||
rows, err := server.db.Query(`
|
||||
SELECT name, flags, key, topic, user_limit
|
||||
FROM channel`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for rows.Next() {
|
||||
var name, flags, key, topic string
|
||||
var userLimit uint64
|
||||
err = rows.Scan(&name, &flags, &key, &topic, &userLimit)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
channel := NewChannel(server, name)
|
||||
for _, flag := range flags {
|
||||
channel.flags[ChannelMode(flag)] = true
|
||||
}
|
||||
channel.key = key
|
||||
channel.topic = topic
|
||||
channel.userLimit = userLimit
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) processCommand(cmd Command) {
|
||||
client := cmd.Client()
|
||||
if DEBUG_SERVER {
|
||||
@ -97,8 +139,14 @@ func (server *Server) processCommand(cmd Command) {
|
||||
}
|
||||
|
||||
func (server *Server) Run() {
|
||||
for {
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case <-server.signals:
|
||||
server.db.Close()
|
||||
done = true
|
||||
continue
|
||||
|
||||
case conn := <-server.newConns:
|
||||
NewClient(server, conn)
|
||||
|
||||
|
13
irc/types.go
13
irc/types.go
@ -106,6 +106,19 @@ func (clients ClientNameMap) Remove(client *Client) error {
|
||||
|
||||
type ChannelModeSet map[ChannelMode]bool
|
||||
|
||||
func (set ChannelModeSet) String() string {
|
||||
if len(set) == 0 {
|
||||
return ""
|
||||
}
|
||||
strs := make([]string, len(set))
|
||||
index := 0
|
||||
for mode := range set {
|
||||
strs[index] = mode.String()
|
||||
index += 1
|
||||
}
|
||||
return strings.Join(strs, "")
|
||||
}
|
||||
|
||||
type ClientSet map[*Client]bool
|
||||
|
||||
func (clients ClientSet) Add(client *Client) {
|
||||
|
10
sql/drop.sql
10
sql/drop.sql
@ -1,10 +0,0 @@
|
||||
DROP INDEX IF EXISTS index_user_id_channel_id;
|
||||
DROP TABLE IF EXISTS user_channel;
|
||||
|
||||
DROP INDEX IF EXISTS index_channel_name;
|
||||
DROP INDEX IF EXISTS index_channel_id;
|
||||
DROP TABLE IF EXISTS channel;
|
||||
|
||||
DROP INDEX IF EXISTS index_user_nick;
|
||||
DROP INDEX IF EXISTS index_user_id;
|
||||
DROP TABLE IF EXISTS user;
|
20
sql/init.sql
20
sql/init.sql
@ -1,20 +0,0 @@
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
nick TEXT NOT NULL UNIQUE,
|
||||
hash BLOB NOT NULL
|
||||
);
|
||||
CREATE INDEX index_user_id ON user(id);
|
||||
CREATE INDEX index_user_nick ON user(nick);
|
||||
|
||||
CREATE TABLE channel (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
CREATE INDEX index_channel_id ON channel(id);
|
||||
|
||||
CREATE TABLE user_channel (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
channel_id INTEGER NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX index_user_id_channel_id ON user_channel (user_id, channel_id);
|
Loading…
Reference in New Issue
Block a user