honk.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18import (
19 "flag"
20 "fmt"
21 "html/template"
22 golog "log"
23 "log/syslog"
24 notrand "math/rand"
25 "os"
26 "strconv"
27 "strings"
28 "time"
29
30 "humungus.tedunangst.com/r/webs/httpsig"
31 "humungus.tedunangst.com/r/webs/log"
32)
33
34var softwareVersion = "develop"
35
36func init() {
37 notrand.Seed(time.Now().Unix())
38}
39
40type WhatAbout struct {
41 ID int64
42 Name string
43 Display string
44 About string
45 HTAbout template.HTML
46 Onts []string
47 Key string
48 URL string
49 Options UserOptions
50 SecKey httpsig.PrivateKey
51}
52
53type UserOptions struct {
54 SkinnyCSS bool `json:",omitempty"`
55 OmitImages bool `json:",omitempty"`
56 MentionAll bool `json:",omitempty"`
57 InlineQuotes bool `json:",omitempty"`
58 Avatar string `json:",omitempty"`
59 Banner string `json:",omitempty"`
60 MapLink string `json:",omitempty"`
61 Reaction string `json:",omitempty"`
62 MeCount int64
63 ChatCount int64
64}
65
66type KeyInfo struct {
67 keyname string
68 seckey httpsig.PrivateKey
69}
70
71const serverUID int64 = -2
72const readyLuserOne int64 = 1
73
74type Honk struct {
75 ID int64
76 UserID int64
77 Username string
78 What string
79 Honker string
80 Handle string
81 Handles string
82 Oonker string
83 Oondle string
84 XID string
85 RID string
86 Date time.Time
87 URL string
88 Noise string
89 Precis string
90 Format string
91 Convoy string
92 Audience []string
93 Public bool
94 Whofore int64
95 Replies []*Honk
96 Flags int64
97 HTPrecis template.HTML
98 HTML template.HTML
99 Style string
100 Open string
101 Donks []*Donk
102 Onts []string
103 Place *Place
104 Time *Time
105 Mentions []Mention
106 Badonks []Badonk
107}
108
109type Badonk struct {
110 Who string
111 What string
112}
113
114type Chonk struct {
115 ID int64
116 UserID int64
117 XID string
118 Who string
119 Target string
120 Date time.Time
121 Noise string
122 Format string
123 Donks []*Donk
124 Handle string
125 HTML template.HTML
126}
127
128type Chatter struct {
129 Target string
130 Chonks []*Chonk
131}
132
133type Mention struct {
134 Who string
135 Where string
136}
137
138func (mention *Mention) IsPresent(noise string) bool {
139 nick := strings.TrimLeft(mention.Who, "@")
140 idx := strings.IndexByte(nick, '@')
141 if idx != -1 {
142 nick = nick[:idx]
143 }
144 return strings.Contains(noise, ">@"+nick) || strings.Contains(noise, "@<span>"+nick)
145}
146
147type OldRevision struct {
148 Precis string
149 Noise string
150}
151
152const (
153 flagIsAcked = 1
154 flagIsBonked = 2
155 flagIsSaved = 4
156 flagIsUntagged = 8
157 flagIsReacted = 16
158 flagIsWonked = 32
159)
160
161func (honk *Honk) IsAcked() bool {
162 return honk.Flags&flagIsAcked != 0
163}
164
165func (honk *Honk) IsBonked() bool {
166 return honk.Flags&flagIsBonked != 0
167}
168
169func (honk *Honk) IsSaved() bool {
170 return honk.Flags&flagIsSaved != 0
171}
172
173func (honk *Honk) IsUntagged() bool {
174 return honk.Flags&flagIsUntagged != 0
175}
176
177func (honk *Honk) IsReacted() bool {
178 return honk.Flags&flagIsReacted != 0
179}
180
181type Donk struct {
182 FileID int64
183 XID string
184 Name string
185 Desc string
186 URL string
187 Media string
188 Local bool
189 External bool
190}
191
192type Place struct {
193 Name string
194 Latitude float64
195 Longitude float64
196 Url string
197}
198
199type Duration int64
200
201func (d Duration) String() string {
202 s := time.Duration(d).String()
203 if strings.HasSuffix(s, "m0s") {
204 s = s[:len(s)-2]
205 }
206 if strings.HasSuffix(s, "h0m") {
207 s = s[:len(s)-2]
208 }
209 return s
210}
211
212func parseDuration(s string) time.Duration {
213 didx := strings.IndexByte(s, 'd')
214 if didx != -1 {
215 days, _ := strconv.ParseInt(s[:didx], 10, 0)
216 dur, _ := time.ParseDuration(s[didx:])
217 return dur + 24*time.Hour*time.Duration(days)
218 }
219 dur, _ := time.ParseDuration(s)
220 return dur
221}
222
223type Time struct {
224 StartTime time.Time
225 EndTime time.Time
226 Duration Duration
227}
228
229type Honker struct {
230 ID int64
231 UserID int64
232 Name string
233 XID string
234 Handle string
235 Flavor string
236 Combos []string
237 Meta HonkerMeta
238}
239
240type HonkerMeta struct {
241 Notes string
242}
243
244type SomeThing struct {
245 What int
246 XID string
247 Owner string
248 Name string
249}
250
251const (
252 SomeNothing int = iota
253 SomeActor
254 SomeCollection
255)
256
257var serverName string
258var serverPrefix string
259var masqName string
260var dataDir = "."
261var viewDir = "."
262var iconName = "icon.png"
263var serverMsg template.HTML
264var aboutMsg template.HTML
265var loginMsg template.HTML
266
267func ElaborateUnitTests() {
268}
269
270func unplugserver(hostname string) {
271 db := opendatabase()
272 xid := fmt.Sprintf("%%https://%s/%%", hostname)
273 db.Exec("delete from honkers where xid like ? and flavor = 'dub'", xid)
274 db.Exec("delete from doovers where rcpt like ?", xid)
275}
276
277func reexecArgs(cmd string) []string {
278 args := []string{"-datadir", dataDir}
279 args = append(args, log.Args()...)
280 args = append(args, cmd)
281 return args
282}
283
284var elog, ilog, dlog *golog.Logger
285
286func main() {
287 flag.StringVar(&dataDir, "datadir", dataDir, "data directory")
288 flag.StringVar(&viewDir, "viewdir", viewDir, "view directory")
289 flag.Parse()
290
291 log.Init(log.Options{Progname: "honk", Facility: syslog.LOG_UUCP})
292 elog = log.E
293 ilog = log.I
294 dlog = log.D
295
296 args := flag.Args()
297 cmd := "run"
298 if len(args) > 0 {
299 cmd = args[0]
300 }
301 switch cmd {
302 case "init":
303 initdb()
304 case "upgrade":
305 upgradedb()
306 case "version":
307 fmt.Println(softwareVersion)
308 os.Exit(0)
309 }
310 db := opendatabase()
311 dbversion := 0
312 getconfig("dbversion", &dbversion)
313 if dbversion != myVersion {
314 elog.Fatal("incorrect database version. run upgrade.")
315 }
316 getconfig("servermsg", &serverMsg)
317 getconfig("aboutmsg", &aboutMsg)
318 getconfig("loginmsg", &loginMsg)
319 getconfig("servername", &serverName)
320 getconfig("masqname", &masqName)
321 if masqName == "" {
322 masqName = serverName
323 }
324 serverPrefix = fmt.Sprintf("https://%s/", serverName)
325 getconfig("usersep", &userSep)
326 getconfig("honksep", &honkSep)
327 getconfig("devel", &develMode)
328 getconfig("fasttimeout", &fastTimeout)
329 getconfig("slowtimeout", &slowTimeout)
330 getconfig("signgets", &signGets)
331 prepareStatements(db)
332 switch cmd {
333 case "admin":
334 adminscreen()
335 case "import":
336 if len(args) != 4 {
337 elog.Fatal("import username mastodon|twitter srcdir")
338 }
339 importMain(args[1], args[2], args[3])
340 case "devel":
341 if len(args) != 2 {
342 elog.Fatal("need an argument: devel (on|off)")
343 }
344 switch args[1] {
345 case "on":
346 setconfig("devel", 1)
347 case "off":
348 setconfig("devel", 0)
349 default:
350 elog.Fatal("argument must be on or off")
351 }
352 case "setconfig":
353 if len(args) != 3 {
354 elog.Fatal("need an argument: setconfig key val")
355 }
356 var val interface{}
357 var err error
358 if val, err = strconv.Atoi(args[2]); err != nil {
359 val = args[2]
360 }
361 setconfig(args[1], val)
362 case "adduser":
363 adduser()
364 case "deluser":
365 if len(args) < 2 {
366 fmt.Printf("usage: honk deluser username\n")
367 return
368 }
369 deluser(args[1])
370 case "chpass":
371 if len(args) < 2 {
372 fmt.Printf("usage: honk chpass username\n")
373 return
374 }
375 chpass(args[1])
376 case "follow":
377 if len(args) < 3 {
378 fmt.Printf("usage: honk follow username url\n")
379 return
380 }
381 user, err := butwhatabout(args[1])
382 if err != nil {
383 fmt.Printf("user not found")
384 return
385 }
386 var meta HonkerMeta
387 mj, _ := jsonify(&meta)
388 honkerid, err := savehonker(user, args[2], "", "presub", "", mj)
389 if err != nil {
390 fmt.Printf("had some trouble with that: %s", err)
391 return
392 }
393 followyou(user, honkerid, true)
394 case "unfollow":
395 if len(args) < 3 {
396 fmt.Printf("usage: honk unfollow username url\n")
397 return
398 }
399 user, err := butwhatabout(args[1])
400 if err != nil {
401 fmt.Printf("user not found")
402 return
403 }
404 row := db.QueryRow("select honkerid from honkers where xid = ? and userid = ?", args[2], user.ID)
405 var honkerid int64
406 err = row.Scan(&honkerid)
407 if err != nil {
408 fmt.Printf("sorry couldn't find them")
409 return
410 }
411 unfollowyou(user, honkerid, true)
412 case "cleanup":
413 arg := "30"
414 if len(args) > 1 {
415 arg = args[1]
416 }
417 cleanupdb(arg)
418 case "unplug":
419 if len(args) < 2 {
420 fmt.Printf("usage: honk unplug servername\n")
421 return
422 }
423 name := args[1]
424 unplugserver(name)
425 case "backup":
426 if len(args) < 2 {
427 fmt.Printf("usage: honk backup dirname\n")
428 return
429 }
430 name := args[1]
431 svalbard(name)
432 case "ping":
433 if len(args) < 3 {
434 fmt.Printf("usage: honk ping (from username) (to username or url)\n")
435 return
436 }
437 name := args[1]
438 targ := args[2]
439 user, err := butwhatabout(name)
440 if err != nil {
441 elog.Printf("unknown user")
442 return
443 }
444 ping(user, targ)
445 case "run":
446 serve()
447 case "backend":
448 backendServer()
449 case "test":
450 ElaborateUnitTests()
451 default:
452 elog.Fatal("unknown command")
453 }
454}